// TODAY${h(shortDate(todayWindow.start).toUpperCase())}
@@ -422,6 +425,7 @@ async function dispatch(action: string, element: HTMLElement) {
switch (action) {
case "nav": view = element.dataset.view as View; editingId = null; render(); break;
case "duration": selectedDuration = Number(element.dataset.minutes); render(); break;
+ case "pick-tag": selectedTag = normalizeTag(element.dataset.tag || selectedTag); render(); break;
case "start": await startSession(); break;
case "stop": await stopSession(); break;
case "prev-log": logMonthOffset--; render(); break;
diff --git a/src/client/recent-tags.ts b/src/client/recent-tags.ts
new file mode 100644
index 0000000..681d0cd
--- /dev/null
+++ b/src/client/recent-tags.ts
@@ -0,0 +1,12 @@
+type TaggedRecord = { startMs: number; tag: string; deleted: boolean };
+
+/** The different tags of the most recent sessions, newest first. */
+export function recentTags(records: TaggedRecord[], limit = 3): string[] {
+ const tags: string[] = [];
+ for (const record of [...records].sort((a, b) => b.startMs - a.startMs)) {
+ if (record.deleted || tags.includes(record.tag)) continue;
+ tags.push(record.tag);
+ if (tags.length === limit) break;
+ }
+ return tags;
+}
diff --git a/src/client/styles.css b/src/client/styles.css
index f29d9de..b534948 100644
--- a/src/client/styles.css
+++ b/src/client/styles.css
@@ -120,7 +120,11 @@ h2 { font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacin
.field::placeholder { color: var(--faint); }
select.field { appearance: none; background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%); background-position: calc(100% - 20px) 20px, calc(100% - 15px) 20px; background-size: 5px 5px; background-repeat: no-repeat; padding-right: 35px; }
.field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
-.duration-row { display: flex; gap: 7px; flex-wrap: wrap; margin-top: 9px; }
+.chip-row { display: flex; gap: 7px; flex-wrap: wrap; margin-top: 9px; }
+/* Minute buttons share the column width, so all six fit on one line in a narrow column. */
+.chip-row.fill { gap: 6px; }
+.chip-row.fill .chip { flex: 1 0 auto; padding-inline: 4px; }
+.tag-chip { max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chip { border: 1px solid var(--line); background: transparent; color: var(--muted); min-height: 34px; padding: 5px 10px; font-size: 11px; }
.chip:hover { border-color: var(--accent); color: var(--accent); }
.chip.active { border-color: var(--accent); color: var(--accent-ink); background: var(--accent); }
diff --git a/tests/recent-tags.test.ts b/tests/recent-tags.test.ts
new file mode 100644
index 0000000..bc28532
--- /dev/null
+++ b/tests/recent-tags.test.ts
@@ -0,0 +1,35 @@
+import { describe, expect, test } from "bun:test";
+import { recentTags } from "../src/client/recent-tags.ts";
+
+const at = (minute: number, tag: string, deleted = false) => ({ startMs: minute * 60_000, tag, deleted });
+
+describe("recent tags", () => {
+ test("gives the 3 different tags from the most recent sessions, newest first", () => {
+ const records = [at(1, "Read"), at(2, "Study"), at(3, "Gym"), at(4, "Study"), at(5, "Code"), at(6, "Study")];
+ expect(recentTags(records)).toEqual(["Study", "Code", "Gym"]);
+ });
+
+ test("does not depend on the order of the records", () => {
+ expect(recentTags([at(9, "Code"), at(1, "Read"), at(5, "Study")])).toEqual(["Code", "Study", "Read"]);
+ });
+
+ test("ignores deleted sessions", () => {
+ expect(recentTags([at(1, "Read"), at(2, "Study", true)])).toEqual(["Read"]);
+ });
+
+ test("gives fewer tags when there are fewer than 3", () => {
+ expect(recentTags([at(1, "Study"), at(2, "Study")])).toEqual(["Study"]);
+ expect(recentTags([])).toEqual([]);
+ });
+});
+
+test("the timer form puts each row of buttons under its field", async () => {
+ const source = await Bun.file("src/client/main.ts").text();
+ const form = source.slice(source.indexOf('id="timer-tag"'), source.indexOf('data-action="start"'));
+ const tagRow = form.indexOf('data-action="pick-tag"');
+ const durationField = form.indexOf('id="timer-duration"');
+ const minuteRow = form.indexOf('data-action="duration"');
+ expect(tagRow).toBeGreaterThan(-1);
+ expect(tagRow).toBeLessThan(durationField);
+ expect(minuteRow).toBeGreaterThan(durationField);
+});