Time log
StableWhat was focused on, by day with daily totals and a seven-day chart, with time added by hand and Undo on delete.
TimeLog shows the provider’s log: the last seven days as a small chart, then the entries grouped by day, newest first, with each day’s total (breaks left out). “Add time” (or N) opens a dialog for work done away from the timer. TimeLogList and TimeLogRow draw the same list from entries you pass, without a provider.
A past week
import type { TimeEntry } from '@rchursin/product/data/sample'import { SampleBadge } from '@rchursin/product/shell/layout'import { TimeLogList } from '@rchursin/product/timer'
/** A past week, read on the Monday after: fixed dates, so the list reads the same on every visit */const NOW = new Date(2026, 8, 14, 9, 0)const at = (day: number, h: number, m: number) => new Date(2026, 8, day, h, m).getTime()
const entries: TimeEntry[] = [ { id: 'w1', taskId: 'RC-122', label: 'Release-notes agent drafts in the wrong tone', start: at(11, 10, 0), end: at(11, 10, 25), kind: 'focus' }, { id: 'w2', taskId: null, label: 'Break', start: at(11, 10, 25), end: at(11, 10, 30), kind: 'break' }, { id: 'w3', taskId: 'RC-122', label: 'Release-notes agent drafts in the wrong tone', start: at(11, 10, 30), end: at(11, 10, 55), kind: 'focus' }, { id: 'w4', taskId: 'RC-117', label: 'Context tax over 8k tokens per turn', start: at(10, 15, 0), end: at(10, 15, 40), kind: 'manual' }, { id: 'w5', taskId: 'RC-123', label: 'Write the skill naming guide', start: at(10, 9, 30), end: at(10, 9, 55), kind: 'focus' }, { id: 'w6', taskId: 'RC-125', label: 'Review pr-review prompt cost', start: at(8, 13, 0), end: at(8, 13, 45), kind: 'manual' }, { id: 'w7', taskId: 'RC-127', label: 'Cut api-docs skill: no runs in 45 days', start: at(8, 16, 10), end: at(8, 16, 35), kind: 'focus' },]
/** * The list on its own, without a provider: pass the entries and a way to look up their tasks. * No onDelete, so the rows are read-only, as in a report. initialDays keeps older days behind a button. */export default function TimeLogDays() { return ( <div className="flex w-full max-w-md flex-col gap-4"> {/* The day headings are h4, so the list sits under an h3 */} <div className="flex items-center justify-between gap-3"> <h3 className="text-[15px] font-semibold tracking-[-0.01em]">Week of Sep 7</h3> <SampleBadge /> </div> <TimeLogList entries={entries} taskById={() => null} now={NOW} initialDays={2} /> </div> )}Usage
import { EmptyLog, FocusProvider, TimeLog, TimeLogList, TimeLogRow } from '@rchursin/product/timer'<FocusProvider> <TimeLog /></FocusProvider>Examples
The live log
TimeLog under a provider, with a week of sample entries. Press Add time (or N inside the log), or delete an entry: focus moves to the next one and the toast offers Undo.
import { SampleBadge } from '@rchursin/product/shell/layout'import { FocusProvider, TimeLog } from '@rchursin/product/timer'
/** * The last seven days as a chart, then the entries by day with a total for each. Add time by hand * with the button (or N while focus is in the log); delete an entry and the toast offers Undo. */export default function TimeLogExample() { return ( <FocusProvider storagePrefix="library:time-log"> <div className="lux-card flex w-full max-w-md flex-col gap-4 rounded-2xl bg-raised p-5"> <SampleBadge className="self-start" /> <TimeLog /> </div> </FocusProvider> )}API
TimeLog props
| Prop | Type | Default | Description |
|---|---|---|---|
summary | boolean | true | Show the seven-day chart above the list. |
className | string | – | Classes on the section, which is also the keyboard scope for N. |
TimeLogList props
| Prop | Type | Default | Description |
|---|---|---|---|
entriesrequired | TimeEntry[] | – | { id, taskId, label, start, end, kind } with kind focus, break or manual. Any order; the list sorts newest first. |
taskByIdrequired | (id: string | null) => Task | null | – | Finds an entry’s task, for its title. Return null to use the entry’s label. |
onDelete | (entry: TimeEntry) => void | – | Shows a delete button on each row. Without it the rows are read-only. |
empty | ReactNode | – | Shown when there are no entries. Defaults to <EmptyLog />. |
now | Date | new Date() | Days are labelled against it: Today, Yesterday, then “Sun, Oct 11”. |
initialDays | number | Infinity | Show this many days, with a “Show 3 earlier days” button for the rest. |
className | string | – | Classes on the wrapper. |
TimeLogRow props
| Prop | Type | Default | Description |
|---|---|---|---|
entryrequired | TimeEntry | – | One entry. Render rows inside a <ul>. |
task | Task | null | – | Its task, for the title. |
onDelete | (entry: TimeEntry) => void | – | Adds the delete button. |
EmptyLog props
| Prop | Type | Default | Description |
|---|---|---|---|
action | ReactNode | – | The way forward, e.g. an “Add time” button. |
className | string | – | Classes on the empty state. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| N | Open Add time, while focus is inside the log. |
| Tab | Moves through Add time, the seven-day chart (every day’s value shows while it has focus), each entry’s delete button and Show earlier days. |
| EnterSpace | On a delete button: delete the entry. Focus moves to the next entry’s delete button (or the previous one, or Add time). |
| Esc | Close the Add time dialog; focus goes back to where it was (the Add time button, if you pressed it). |
- The log is a section named by its “Time log” heading; each day is a section with its own heading, and its total says “Total, breaks left out” to screen readers.
- Entry kinds differ by shape as well as icon and name: focus is a filled tile, time added by hand an outlined one, a break a dashed one. The icon carries the kind’s name.
- Delete buttons are named in full (“Delete focus session: Fix test-fixer, 9:05 – 9:30 AM”). On devices with hover they appear on hover or keyboard focus; on touch screens they are always there.
- Add time is a modal dialog that a stray click outside won’t close. Errors are written next to their fields, and a failed save puts focus on the first one to fix.
- Deleting and adding end in a toast with Undo.
Guidelines
Do
- Keep the log next to the timer that fills it; under the same FocusProvider it updates the moment a phase is logged.
- Use TimeLogList for a read-only history (reports, a project page) and pass onDelete only where deleting makes sense.
Don’t
- Don’t count breaks in totals; the log and the chart leave them out on purpose.
- Don’t accept time that hasn’t happened yet; the dialog refuses an end time in the future.
In use
Related
- TimerThe Focus screen: a focus session, a stopwatch and a countdown one switch apart, with the time log beside them.
- Focus sessionA Pomodoro session: choose a task, say what you’ll finish, then focus and break in phases, with overtime and an inline Abandon.
- Weekly chartFocused time over the last seven days as square-topped bars to scale, with a real table underneath for screen readers.
- DialogA modal on the native <dialog> element: the page behind goes inert, Esc closes it, and focus goes back to what opened it.
- ToastShort past-tense results with at most one action, like “Run archived · Undo”. Read out politely; they wait while hovered or focused.
- Empty stateOne heading that says what is missing, one line that says why, and one way forward, centred under an outlined icon tile.
Source
The component’s own files, exactly as they run on this site.
/** * The time log: what was focused on, grouped by day with a total per day, plus the last seven days as a small chart. * Finished focus sessions land here; time worked away from the timer can be added by hand (N). * Deleting is instant with Undo in the toast, and focus moves to the next entry so the keyboard never gets lost. */import { useEffect, useId, useMemo, useRef, useState, type ReactNode, type SubmitEvent,} from "react"import { cx } from "../../components/cx"import type { Task, TimeEntry } from "../data/sample"import { Icon, type IconName } from "../icons/Icon"import { MINUTE, dayDiff,