diff --git a/app/src/Stats.jsx b/app/src/Stats.jsx
index 24e39ab..2ddb2e4 100644
--- a/app/src/Stats.jsx
+++ b/app/src/Stats.jsx
@@ -398,11 +398,11 @@ function ReadingCalendar({ summary, activity, books = [] }) {
setScale(v)
setBack(0)
}
- const cell = (d, round) => {
- if (!shown(d)) return
+ const cell = (d, round, style) => {
+ if (!shown(d)) return
const key = localDay(d)
const tip = `${fmt(d, { weekday: 'short', month: 'short', day: 'numeric' })}: ${byDay.has(key) ? tipFor(key) : 'no reading'}`
- return
+ return
}
// Share the visible period, reading days, available page/time totals and longest streak.
const share = () => {
@@ -538,14 +538,14 @@ function ReadingCalendar({ summary, activity, books = [] }) {
}}
>
{WEEKDAYS.map((w, r) => (
-
+
{!year || r % 2 === 0 ? w[0] : ''}
))}
-
- {weeks.map((monday) => (
+ {/* Explicit cells: month labels leave gaps in row 8 that auto-placement would fill with days. */}
+ {weeks.map((monday, i) => (
- {WEEKDAYS.map((w, r) => cell(new Date(monday.getTime() + r * DAY_MS), 'rounded-[2px]'))}
+ {WEEKDAYS.map((w, r) => cell(new Date(monday.getTime() + r * DAY_MS), 'rounded-[2px]', { gridRow: r + 1, gridColumn: i + 2 }))}
))}
{/* Labels span weeks so their text fits; keep the last month inside the grid. */}
diff --git a/app/src/Stats.test.jsx b/app/src/Stats.test.jsx
index b4ec3ab..375f7e7 100644
--- a/app/src/Stats.test.jsx
+++ b/app/src/Stats.test.jsx
@@ -487,3 +487,12 @@ describe('BookOrbit stats', () => {
expect(queryByText(/Duplicate BookOrbit session records are counted once/)).toBeNull()
})
})
+
+it('pins every year-calendar day to its weekday row', () => {
+ const { container, getByText } = render()
+ fireEvent.click(getByText('Year'))
+ const cells = [...container.querySelectorAll('[data-tip]')]
+ expect(cells.length).toBeGreaterThan(300)
+ const rows = { Mon: '1', Tue: '2', Wed: '3', Thu: '4', Fri: '5', Sat: '6', Sun: '7' }
+ for (const el of cells) expect(el.style.gridRow).toBe(rows[el.dataset.tip.slice(0, 3)])
+})