Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3424222dab | ||
|
|
e7178c6d3f |
No files matched your search
+8
-4
@@ -29,10 +29,14 @@ export function shiftDays(key: string, days: number): string {
|
||||
return localKey(date);
|
||||
}
|
||||
|
||||
/** Milliseconds until the next local midnight, with a little slack past it. */
|
||||
export function untilMidnight(now = new Date()): number {
|
||||
const next = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
||||
return Math.max(1000, next.getTime() - now.getTime() + 500);
|
||||
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 {
|
||||
|
||||
+5
-13
@@ -5,7 +5,7 @@
|
||||
<script lang="ts">
|
||||
import ChevronLeft from "@lucide/svelte/icons/chevron-left";
|
||||
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 { cn } from "../lib/utils";
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
|
||||
let { anchor, tone, onpick, onhover, today }: Props = $props();
|
||||
|
||||
const DAY = 86_400_000;
|
||||
const monthName = new Intl.DateTimeFormat("en-US", { month: "long" });
|
||||
const todayKey = $derived(today ?? localKey(new Date()));
|
||||
|
||||
@@ -32,20 +31,13 @@
|
||||
return new Date(base.getFullYear(), base.getMonth(), 1);
|
||||
});
|
||||
|
||||
const days = $derived.by(() => {
|
||||
const offset = (month.getDay() + 6) % 7;
|
||||
const start = new Date(month.getTime() - offset * DAY);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
return Array.from({ length: 42 }, (_, index) => {
|
||||
const date = new Date(start.getTime() + index * DAY);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
return {
|
||||
const days = $derived(
|
||||
monthGrid(month).map((date) => ({
|
||||
date,
|
||||
inMonth: date.getMonth() === month.getMonth(),
|
||||
key: localKey(date),
|
||||
};
|
||||
});
|
||||
});
|
||||
})),
|
||||
);
|
||||
|
||||
function shiftMonth(delta: number) {
|
||||
picked = new Date(month.getFullYear(), month.getMonth() + delta, 1);
|
||||
|
||||
+23
-30
@@ -4,7 +4,7 @@ import { isArchivePath, toBoardPath } from "../model/archive";
|
||||
import { parseBoard, sphereOf } from "../model/board";
|
||||
import { anchorDate, isOpen, STATUS_ORDER } from "../model/types";
|
||||
import type { Board, Status, Task } from "../model/types";
|
||||
import { daysFromToday, fromKey, localKey, shiftDays, untilMidnight } from "../lib/due";
|
||||
import { daysFromToday, fromKey, localKey, monthGrid, shiftDays } from "../lib/due";
|
||||
import { laneKey, parseLaneKey } from "../lib/keys";
|
||||
import type {
|
||||
BeaverCalendarSettings,
|
||||
@@ -13,7 +13,6 @@ import type {
|
||||
ViewMode,
|
||||
} from "../settings";
|
||||
|
||||
const DAY = 86_400_000;
|
||||
|
||||
export interface Group {
|
||||
key: string;
|
||||
@@ -197,37 +196,35 @@ export class BoardStore {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
/** Re-reads the wall clock; a no-op while the day has not turned. */
|
||||
/**
|
||||
* Re-reads the wall clock; a no-op while the day has not turned. A cursor
|
||||
* left on the old today, and a month showing it, follow the day over.
|
||||
*/
|
||||
tick(): void {
|
||||
const now = localKey(new Date());
|
||||
if (now !== this.today) this.today = now;
|
||||
const was = this.today;
|
||||
if (now === was) return;
|
||||
this.today = now;
|
||||
if (this.cursorDay === was && this.cursorTask === null) this.cursorDay = now;
|
||||
if (this.monthKey.slice(0, 7) === was.slice(0, 7)) this.monthKey = now;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps `today` honest for as long as the view lives: a timer for the
|
||||
* midnight crossing, and a re-read when the window wakes up, since a
|
||||
* sleeping laptop fires no timers.
|
||||
* Keeps `today` honest for as long as the view lives. The clock is polled
|
||||
* rather than timed to midnight: Chromium's timers stop while a Mac sleeps,
|
||||
* so a midnight timeout fires hours late, and a window that stayed focused
|
||||
* through the sleep sends no focus event on waking. Waking still re-reads
|
||||
* at once where the events do come.
|
||||
*/
|
||||
watchClock(): () => void {
|
||||
let timer = 0;
|
||||
const arm = (): void => {
|
||||
window.clearTimeout(timer);
|
||||
timer = window.setTimeout(() => {
|
||||
this.tick();
|
||||
arm();
|
||||
}, untilMidnight());
|
||||
};
|
||||
const wake = (): void => {
|
||||
this.tick();
|
||||
arm();
|
||||
};
|
||||
arm();
|
||||
window.addEventListener("focus", wake);
|
||||
document.addEventListener("visibilitychange", wake);
|
||||
const tick = (): void => this.tick();
|
||||
const timer = window.setInterval(tick, 30_000);
|
||||
window.addEventListener("focus", tick);
|
||||
document.addEventListener("visibilitychange", tick);
|
||||
return () => {
|
||||
window.clearTimeout(timer);
|
||||
window.removeEventListener("focus", wake);
|
||||
document.removeEventListener("visibilitychange", wake);
|
||||
window.clearInterval(timer);
|
||||
window.removeEventListener("focus", tick);
|
||||
document.removeEventListener("visibilitychange", tick);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -451,13 +448,9 @@ export class BoardStore {
|
||||
monthDays = $derived.by<DaySlot[]>(() => {
|
||||
const anchor = fromKey(this.monthKey);
|
||||
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 byDay = this.byDay;
|
||||
return Array.from({ length: 42 }, (_, index) => {
|
||||
const date = new Date(start.getTime() + index * DAY);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
return monthGrid(first).map((date) => {
|
||||
const key = localKey(date);
|
||||
return {
|
||||
date,
|
||||
|
||||
+28
-8
@@ -1,10 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
daysFromToday,
|
||||
dueLabel,
|
||||
dueTone,
|
||||
localKey,
|
||||
monthGrid,
|
||||
nextFriday,
|
||||
untilMidnight,
|
||||
} from "../src/lib/due";
|
||||
|
||||
describe("due helpers read the day they are given", () => {
|
||||
@@ -26,12 +27,31 @@ describe("due helpers read the day they are given", () => {
|
||||
expect(nextFriday("2026-08-23")).toBe("2026-08-28");
|
||||
expect(nextFriday("2026-08-28")).toBe("2026-09-04");
|
||||
});
|
||||
});
|
||||
|
||||
it("arms the midnight timer for the coming midnight", () => {
|
||||
const late = new Date(2026, 7, 23, 23, 59, 0);
|
||||
expect(untilMidnight(late)).toBeGreaterThan(60_000);
|
||||
expect(untilMidnight(late)).toBeLessThan(62_000);
|
||||
const early = new Date(2026, 7, 23, 0, 0, 1);
|
||||
expect(untilMidnight(early)).toBeGreaterThan(23 * 3_600_000);
|
||||
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