diff --git a/src/client/components/heatmap.tsx b/src/client/components/heatmap.tsx index b07b6be..9c04234 100644 --- a/src/client/components/heatmap.tsx +++ b/src/client/components/heatmap.tsx @@ -1,6 +1,7 @@ /** * The Context Dashboard's activity heatmap: a GitHub-style contribution grid - * (weeks as columns, Monday-first weekdays as rows) over the merged daily + * (weeks as columns, Monday-first weekdays as rows, a month label over the + * column where each month begins) over the merged daily * ledger (overview.ts). Cell depth is the day's billed-token share of the * window's maximum, in four steps; a day with data is a button whose click * pins the session list to that day (click again to release). Cells tip @@ -107,34 +108,42 @@ export function makeHeatmap(kit: ViewKit): (props: HeatmapProps) => ReactElement ))}
- {columns.map((column, wi) => ( -
- {column.map((cell) => { - if (cell.future) return
+ ) + })}
) diff --git a/src/client/i18n.ts b/src/client/i18n.ts index 9bcefb9..c90a91a 100644 --- a/src/client/i18n.ts +++ b/src/client/i18n.ts @@ -261,6 +261,10 @@ export const DICT_ZH: Record = { 'ov.heat.empty': '暂无活跃数据 — 会话产生模型请求后,这里会显示每日用量', 'ov.heat.sessions': '{n} 个活跃会话', 'ov.heat.wd.1': '一', 'ov.heat.wd.3': '三', 'ov.heat.wd.5': '五', + 'ov.heat.mon.01': '1月', 'ov.heat.mon.02': '2月', 'ov.heat.mon.03': '3月', + 'ov.heat.mon.04': '4月', 'ov.heat.mon.05': '5月', 'ov.heat.mon.06': '6月', + 'ov.heat.mon.07': '7月', 'ov.heat.mon.08': '8月', 'ov.heat.mon.09': '9月', + 'ov.heat.mon.10': '10月', 'ov.heat.mon.11': '11月', 'ov.heat.mon.12': '12月', 'ov.list.title': '会话', 'ov.list.search': '搜索标题或目录…', 'ov.list.sortLabel': '排序', @@ -540,6 +544,10 @@ export const DICT_EN: Record = { 'ov.heat.empty': 'No activity yet — daily usage appears once sessions make model requests', 'ov.heat.sessions': '{n} active sessions', 'ov.heat.wd.1': 'M', 'ov.heat.wd.3': 'W', 'ov.heat.wd.5': 'F', + 'ov.heat.mon.01': 'Jan', 'ov.heat.mon.02': 'Feb', 'ov.heat.mon.03': 'Mar', + 'ov.heat.mon.04': 'Apr', 'ov.heat.mon.05': 'May', 'ov.heat.mon.06': 'Jun', + 'ov.heat.mon.07': 'Jul', 'ov.heat.mon.08': 'Aug', 'ov.heat.mon.09': 'Sep', + 'ov.heat.mon.10': 'Oct', 'ov.heat.mon.11': 'Nov', 'ov.heat.mon.12': 'Dec', 'ov.list.title': 'Sessions', 'ov.list.search': 'Search title or path…', 'ov.list.sortLabel': 'Sort', diff --git a/src/client/styles/overview.css b/src/client/styles/overview.css index 73acdd3..2d662ef 100644 --- a/src/client/styles/overview.css +++ b/src/client/styles/overview.css @@ -71,13 +71,23 @@ * The activity heatmap: fixed-size square cells (8 week columns ≈ a 200px * block) centered in the card, so the card's height follows the 7 cell rows * instead of the cells stretching to the card's width — the compact - * GitHub-widget read. Monday-first, four depth steps. + * GitHub-widget read. Monday-first, four depth steps, a month label over + * the column where each month begins. */ .lc-heat { display: flex; gap: 4px; flex: 1; align-items: center; justify-content: center; } +/* Each column and the weekday gutter reserve the same 13px line above their + cells — the month labels' line — so the centered cell rows stay aligned. */ +.lc-heat-wds, .lc-heat-col { padding-top: 13px; } .lc-heat-wds { display: flex; flex-direction: column; gap: 3px; } .lc-heat-wd { flex: none; width: 10px; height: 20px; display: flex; align-items: center; justify-content: center; font-size: 9px; color: var(--dsw-alias-label-secondary); text-align: center; } .lc-heat-cols { display: flex; gap: 3px; } -.lc-heat-col { display: flex; flex-direction: column; gap: 3px; } +/* relative anchors the label; absolute keeps its width off the 20px column + (a wider in-flow label would widen just the labeled columns and skew the + grid). top:0 rides the column's own 13px padding band, clear of the cells. + Labels only sit at month opens, ≥4 columns apart, so the nowrap overflow + past a 20px column never meets the next label. */ +.lc-heat-col { position: relative; display: flex; flex-direction: column; gap: 3px; } +.lc-heat-mon { position: absolute; top: 0; left: 0; font-size: 9px; line-height: 13px; color: var(--dsw-alias-label-secondary); white-space: nowrap; } .lc-heat-cell { width: 20px; height: 20px; box-sizing: border-box; border: 0; border-radius: 20%; padding: 0; background: var(--dsw-alias-bg-layer-2); cursor: default; } button.lc-heat-cell { cursor: pointer; } button.lc-heat-cell:hover { box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--color-blue-500) 70%, transparent); } diff --git a/tests/client/components/heatmap.spec.ts b/tests/client/components/heatmap.spec.ts index 8237e33..d30b025 100644 --- a/tests/client/components/heatmap.spec.ts +++ b/tests/client/components/heatmap.spec.ts @@ -84,6 +84,24 @@ describe('Heatmap', () => { await m.unmount() }) + test('month labels mark the columns where a month begins', async () => { + const days = { '2026-09-16': { tokens: 10, requests: 1, sessions: 1 } } + // The default 8-week window runs Monday 2026-07-27 → 2026-09-14: July + // began outside it, August begins in the Jul 27 column (Sat Aug 1) and + // September in the Aug 31 column (Tue Sep 1). + const m = await mount(h(Heatmap, { days, today: TODAY })) + assert.deepEqual(queryAll(m.container, '.lc-heat-mon').map(s => s.textContent), ['Aug', 'Sep']) + await m.unmount() + // A window without any month's first day draws no labels at all. + const m2 = await mount(h(Heatmap, { days, today: TODAY, weeks: 2 })) + assert.deepEqual(queryAll(m2.container, '.lc-heat-mon').map(s => s.textContent), []) + await m2.unmount() + // A column that opens on the 1st carries the label itself. + const m3 = await mount(h(Heatmap, { days: { '2026-06-03': { tokens: 1, requests: 1, sessions: 1 } }, today: '2026-06-03', weeks: 1 })) + assert.deepEqual(queryAll(m3.container, '.lc-heat-mon').map(s => s.textContent), ['Jun']) + await m3.unmount() + }) + test('cells tip through the harness Tooltip: the bubble mounts on hover and drops on leave', async () => { const m = await mount(h(Heatmap, { days: { '2026-09-16': { tokens: 10, requests: 1, sessions: 3 } },