fix(due,store,ui): month grid steps by calendar day so dst cannot repeat one
This commit is contained in:
1 parent
e7178c6d3f
commit
3424222dab
4 files changed
+50
-24
No files matched your search
@@ -29,6 +29,16 @@ export function shiftDays(key: string, days: number): string {
|
|||||||
return localKey(date);
|
return localKey(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function monthGrid(month: Date): Date[] {
|
||||||
|
const year = month.getFullYear();
|
||||||
|
const index = month.getMonth();
|
||||||
|
const offset = (new Date(year, index, 1).getDay() + 6) % 7;
|
||||||
|
return Array.from(
|
||||||
|
{ length: 42 },
|
||||||
|
(_, cell) => new Date(year, index, 1 - offset + cell),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function daysFromToday(key: string, from = today()): number {
|
export function daysFromToday(key: string, from = today()): number {
|
||||||
return Math.round((fromKey(key).getTime() - fromKey(from).getTime()) / 86_400_000);
|
return Math.round((fromKey(key).getTime() - fromKey(from).getTime()) / 86_400_000);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-16
@@ -5,7 +5,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ChevronLeft from "@lucide/svelte/icons/chevron-left";
|
import ChevronLeft from "@lucide/svelte/icons/chevron-left";
|
||||||
import ChevronRight from "@lucide/svelte/icons/chevron-right";
|
import ChevronRight from "@lucide/svelte/icons/chevron-right";
|
||||||
import { fromKey, localKey } from "../lib/due";
|
import { fromKey, localKey, monthGrid } from "../lib/due";
|
||||||
import { WEEKDAYS } from "../lib/vocab";
|
import { WEEKDAYS } from "../lib/vocab";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
|
|
||||||
let { anchor, tone, onpick, onhover, today }: Props = $props();
|
let { anchor, tone, onpick, onhover, today }: Props = $props();
|
||||||
|
|
||||||
const DAY = 86_400_000;
|
|
||||||
const monthName = new Intl.DateTimeFormat("en-US", { month: "long" });
|
const monthName = new Intl.DateTimeFormat("en-US", { month: "long" });
|
||||||
const todayKey = $derived(today ?? localKey(new Date()));
|
const todayKey = $derived(today ?? localKey(new Date()));
|
||||||
|
|
||||||
@@ -32,20 +31,13 @@
|
|||||||
return new Date(base.getFullYear(), base.getMonth(), 1);
|
return new Date(base.getFullYear(), base.getMonth(), 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
const days = $derived.by(() => {
|
const days = $derived(
|
||||||
const offset = (month.getDay() + 6) % 7;
|
monthGrid(month).map((date) => ({
|
||||||
const start = new Date(month.getTime() - offset * DAY);
|
date,
|
||||||
start.setHours(0, 0, 0, 0);
|
inMonth: date.getMonth() === month.getMonth(),
|
||||||
return Array.from({ length: 42 }, (_, index) => {
|
key: localKey(date),
|
||||||
const date = new Date(start.getTime() + index * DAY);
|
})),
|
||||||
date.setHours(0, 0, 0, 0);
|
);
|
||||||
return {
|
|
||||||
date,
|
|
||||||
inMonth: date.getMonth() === month.getMonth(),
|
|
||||||
key: localKey(date),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function shiftMonth(delta: number) {
|
function shiftMonth(delta: number) {
|
||||||
picked = new Date(month.getFullYear(), month.getMonth() + delta, 1);
|
picked = new Date(month.getFullYear(), month.getMonth() + delta, 1);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { isArchivePath, toBoardPath } from "../model/archive";
|
|||||||
import { parseBoard, sphereOf } from "../model/board";
|
import { parseBoard, sphereOf } from "../model/board";
|
||||||
import { anchorDate, isOpen, STATUS_ORDER } from "../model/types";
|
import { anchorDate, isOpen, STATUS_ORDER } from "../model/types";
|
||||||
import type { Board, Status, Task } from "../model/types";
|
import type { Board, Status, Task } from "../model/types";
|
||||||
import { daysFromToday, fromKey, localKey, shiftDays } from "../lib/due";
|
import { daysFromToday, fromKey, localKey, monthGrid, shiftDays } from "../lib/due";
|
||||||
import { laneKey, parseLaneKey } from "../lib/keys";
|
import { laneKey, parseLaneKey } from "../lib/keys";
|
||||||
import type {
|
import type {
|
||||||
BeaverCalendarSettings,
|
BeaverCalendarSettings,
|
||||||
@@ -13,7 +13,6 @@ import type {
|
|||||||
ViewMode,
|
ViewMode,
|
||||||
} from "../settings";
|
} from "../settings";
|
||||||
|
|
||||||
const DAY = 86_400_000;
|
|
||||||
|
|
||||||
export interface Group {
|
export interface Group {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -449,13 +448,9 @@ export class BoardStore {
|
|||||||
monthDays = $derived.by<DaySlot[]>(() => {
|
monthDays = $derived.by<DaySlot[]>(() => {
|
||||||
const anchor = fromKey(this.monthKey);
|
const anchor = fromKey(this.monthKey);
|
||||||
const first = new Date(anchor.getFullYear(), anchor.getMonth(), 1);
|
const first = new Date(anchor.getFullYear(), anchor.getMonth(), 1);
|
||||||
const offset = (first.getDay() + 6) % 7;
|
|
||||||
const start = new Date(first.getTime() - offset * DAY);
|
|
||||||
const todayKey = this.today;
|
const todayKey = this.today;
|
||||||
const byDay = this.byDay;
|
const byDay = this.byDay;
|
||||||
return Array.from({ length: 42 }, (_, index) => {
|
return monthGrid(first).map((date) => {
|
||||||
const date = new Date(start.getTime() + index * DAY);
|
|
||||||
date.setHours(0, 0, 0, 0);
|
|
||||||
const key = localKey(date);
|
const key = localKey(date);
|
||||||
return {
|
return {
|
||||||
date,
|
date,
|
||||||
|
|||||||
+30
-1
@@ -1,8 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
daysFromToday,
|
daysFromToday,
|
||||||
dueLabel,
|
dueLabel,
|
||||||
dueTone,
|
dueTone,
|
||||||
|
localKey,
|
||||||
|
monthGrid,
|
||||||
nextFriday,
|
nextFriday,
|
||||||
} from "../src/lib/due";
|
} from "../src/lib/due";
|
||||||
|
|
||||||
@@ -26,3 +28,30 @@ describe("due helpers read the day they are given", () => {
|
|||||||
expect(nextFriday("2026-08-28")).toBe("2026-09-04");
|
expect(nextFriday("2026-08-28")).toBe("2026-09-04");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("monthGrid steps by calendar day", () => {
|
||||||
|
const zone = process.env.TZ;
|
||||||
|
beforeAll(() => {
|
||||||
|
process.env.TZ = "Europe/Berlin";
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
process.env.TZ = zone;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps every day once across the autumn clock change", () => {
|
||||||
|
const keys = monthGrid(new Date(2026, 9, 1)).map(localKey);
|
||||||
|
expect(keys).toHaveLength(42);
|
||||||
|
expect(new Set(keys).size).toBe(42);
|
||||||
|
expect(keys[0]).toBe("2026-09-28");
|
||||||
|
expect(keys[27]).toBe("2026-10-25");
|
||||||
|
expect(keys[28]).toBe("2026-10-26");
|
||||||
|
expect(keys[41]).toBe("2026-11-08");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps every day once across the spring clock change", () => {
|
||||||
|
const keys = monthGrid(new Date(2027, 2, 1)).map(localKey);
|
||||||
|
expect(new Set(keys).size).toBe(42);
|
||||||
|
expect(keys).toContain("2027-03-28");
|
||||||
|
expect(keys).toContain("2027-03-29");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user