diff --git a/src/habits.test.ts b/src/habits.test.ts index 4ea965f..8e526af 100644 --- a/src/habits.test.ts +++ b/src/habits.test.ts @@ -4,6 +4,7 @@ import { toggleCompletion, isCompletedOn, dayStr, + shiftDay, currentStreak, bestStreak, completionsInRange, @@ -47,6 +48,25 @@ describe("dayStr", () => { }); }); +describe("shiftDay", () => { + it("shifts forward by positive delta", () => { + expect(shiftDay("2026-06-01", 1)).toBe("2026-06-02"); + }); + + it("shifts backward by negative delta", () => { + expect(shiftDay("2026-06-01", -1)).toBe("2026-05-31"); + }); + + it("returns the same day for delta 0", () => { + expect(shiftDay("2026-06-15", 0)).toBe("2026-06-15"); + }); + + it("handles month boundaries correctly", () => { + expect(shiftDay("2026-05-31", 1)).toBe("2026-06-01"); + expect(shiftDay("2026-06-01", -29)).toBe("2026-05-03"); + }); +}); + describe("currentStreak", () => { const today = "2026-06-10"; diff --git a/src/habits.ts b/src/habits.ts index 7065e8b..965d7bc 100644 --- a/src/habits.ts +++ b/src/habits.ts @@ -46,7 +46,7 @@ export function toggleCompletion(habit: Habit, day: string): Habit { return { ...habit, completions }; } -function shiftDay(day: string, delta: number): string { +export function shiftDay(day: string, delta: number): string { const d = new Date(day + "T00:00:00Z"); d.setUTCDate(d.getUTCDate() + delta); return d.toISOString().slice(0, 10); diff --git a/src/main.ts b/src/main.ts index d330cff..d15b3ca 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,7 +5,9 @@ import { toggleCompletion, isCompletedOn, dayStr, + shiftDay, currentStreak, + last7Count, } from "./habits"; const STORAGE_KEY = "zenith.habits.v1"; @@ -53,14 +55,24 @@ function render(): void { streak === 0 ? "" : `${streak}${streak >= 3 ? " 🔥" : ""}`; + const heatmapCells = Array.from({ length: 30 }, (_, i) => { + const day = shiftDay(today, i - 29); + const cellDone = isCompletedOn(h, day); + return `
`; + }).join(""); + const weeklyCount = last7Count(h, today); return `
  • - - ${escapeHtml(h.name)} - ${streakHtml} - +
    + + ${escapeHtml(h.name)} + ${streakHtml} + +
    + +
    ${weeklyCount} / 7 this week
  • `; }) .join("") diff --git a/src/style.css b/src/style.css index 1ad56ce..754cf0f 100644 --- a/src/style.css +++ b/src/style.css @@ -82,19 +82,49 @@ form#add button { .habits li { display: flex; - align-items: center; - gap: 12px; + flex-direction: column; + gap: 8px; padding: 12px 14px; background: var(--card); border: 1px solid #2c2f50; border-radius: 14px; } +.habit-row { + display: flex; + align-items: center; + gap: 12px; +} + +.heatmap { + display: grid; + grid-template-columns: repeat(30, 1fr); + gap: 2px; +} + +.heatmap-cell { + aspect-ratio: 1; + border-radius: 2px; + background: #2c2f50; + min-width: 0; +} + +.heatmap-cell.done { + background: var(--accent); +} + +.weekly-stats { + font-size: 0.75rem; + color: var(--muted); + text-align: right; +} + .habits li.done { border-color: var(--accent-2); } .habits li.empty { + align-items: center; justify-content: center; color: var(--muted); border-style: dashed;