Add recent tag buttons under the tag picker

Under the TAG picker, the timer form shows buttons for the 3 different
tags from the most recent sessions, newest first. A tap selects the
tag. It does not start a session.

The minute buttons move under the DURATION field. They share the column
width, so all six stay on one line on desktop and on mobile.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
minhtrannhatandClaude Opus 5.5 committed 2026-11-08 15:10:12 -05:00
1 parent c90c0ddf9d
commit 52e8948d73
4 files changed
+59 -4

No files matched your search

+7 -3
View File
@@ -6,6 +6,7 @@ import { dueMs, recordEnd, type TimeRecord } from "../shared/record.ts";
import { normalizeTag } from "../shared/tag.ts";
import { countdown, pad, pageTitle } from "./title.ts";
import { readyToReload } from "./update.ts";
import { recentTags } from "./recent-tags.ts";
import { THEMES, THEME_COLORS, isTheme, type Theme } from "./theme.ts";
type View = "timer" | "log" | "insights" | "settings";
@@ -144,6 +145,7 @@ function timerPage() {
const completedToday = completed.length;
const week = totals(records, rangeWindow("week"));
const tagOptions = tags().map(tag => `<option value="${h(tag)}" ${tag === selectedTag ? "selected" : ""}>${h(tag)}</option>`).join("");
const recent = recentTags(records);
const latest = completed.sort((a, b) => b.startMs - a.startMs).slice(0, 6);
const weekMax = Math.max(...week.trend, 1);
return `<section class="page">
@@ -155,9 +157,10 @@ function timerPage() {
<div class="progress-track"><div class="progress-fill" id="progress-fill" style="width:${active ? progress(active) : 0}%"></div></div>
${active ? `<div class="field-grid"><div><span class="field-label">TAG</span><div class="field" style="display:flex;align-items:center">${h(active.tag)}</div></div><div><span class="field-label">DURATION</span><div class="field" style="display:flex;align-items:center">${duration(active.targetSeconds! * 1000)}</div></div></div>
<button class="primary-btn outline" data-action="stop" type="button">■ STOP EARLY &amp; LOG TIME</button>` : `
<div class="field-grid"><div><label class="field-label" for="timer-tag">TAG</label><select class="field" id="timer-tag">${tagOptions}<option value="__new__">+ New tag...</option></select></div>
<div><label class="field-label" for="timer-duration">DURATION / MIN</label><input class="field" id="timer-duration" type="number" min="1" max="180" inputmode="numeric" value="${selectedDuration}" /></div></div>
<div class="duration-row">${[15, 25, 30, 45, 60, 120].map(n => `<button class="chip ${n === selectedDuration ? "active" : ""}" data-action="duration" data-minutes="${n}" type="button">${n}m</button>`).join("")}</div>
<div class="field-grid"><div><label class="field-label" for="timer-tag">TAG</label><select class="field" id="timer-tag">${tagOptions}<option value="__new__">+ New tag...</option></select>
${recent.length ? `<div class="chip-row" role="group" aria-label="Recent tags">${recent.map(tag => `<button class="chip tag-chip ${tag === selectedTag ? "active" : ""}" data-action="pick-tag" data-tag="${h(tag)}" title="${h(tag)}" type="button">${h(tag)}</button>`).join("")}</div>` : ""}</div>
<div><label class="field-label" for="timer-duration">DURATION / MIN</label><input class="field" id="timer-duration" type="number" min="1" max="180" inputmode="numeric" value="${selectedDuration}" />
<div class="chip-row fill" role="group" aria-label="Durations">${[15, 25, 30, 45, 60, 120].map(n => `<button class="chip ${n === selectedDuration ? "active" : ""}" data-action="duration" data-minutes="${n}" type="button">${n}m</button>`).join("")}</div></div></div>
<button class="primary-btn" data-action="start" type="button">&gt; START SESSION</button>`}
</div></div>
<div class="panel"><div class="panel-inner"><div class="panel-label"><span>// TODAY</span><span class="end">${h(shortDate(todayWindow.start).toUpperCase())}</span></div>
@@ -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;
+12
View File
@@ -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;
}
+5 -1
View File
@@ -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); }