Track time with one running timer shared by every tab, a weekly timesheet that accepts durations the way people type them, and a day-grouped entry list. Billable time becomes invoice line drafts with decimal-string totals. Attendance sheets use the Resource calendar's codes variant.
Tuesday, November 101 h 40 min
- Mobile appCheckout flow reviewBillable6:30 AM–8:10 AM1:40
Monday, November 92 h
- Website relaunchBooking widget QABillable12:00 PM–1:15 PM1:15
- Partner supportWeekly call8:00 AM–8:45 AM0:45
Installation
The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";
Usage
import { TimerButton, Timesheet, createLocalTimerAdapter } from "@vamos-ui/react/scheduler";
const adapter = createLocalTimerAdapter({ onStop: (entry) => api.timeEntries.create(entry) });
<TimerButton adapter={adapter} projects={projects} /> // top bar
<Timesheet
weekStart="2026-11-09"
rows={projects}
value={minutesByRowAndDay} // { [rowId]: { "2026-11-09": 240 } }
onCellChange={(rowId, day, minutes) => api.timesheet.set(rowId, day, minutes)}
dailyTarget={8 * 60}
/>The timer is a toggle button (aria-pressed) named after its project: “Start timer for Mobile app”. The counter isn't announced every second, only when the timer starts and stops.
The timesheet is a grid with one tab stop. Use the arrow keys to move, type a digit or press Enter to edit, Enter to save (then move down), Escape to cancel and Delete to clear.
Idle detection is planned for a later release.
Examples
Weekly timesheet
An agency week. Type into a cell (1.5, 1:30, 90m, 1h 30m), press Enter to save and move down, and Escape to cancel. Row, day and week totals have their own accessible names. A working day under the 8 h target turns amber.
Timesheet · Nov 9 – Nov 15, 2026
Type a duration, Enter saves, Escape cancelsBillable time → line items
toLineItems groups billable entries by project (or task) and rate. Quantities are hours as decimal strings with unit HUR, ready for an invoice.
| Description | Hours | Rate | Amount |
|---|---|---|---|
| Mobile app | 5.83 | €95.00 | €553.85 |
| Website relaunch | 4 | €85.00 | €340.00 |
| Total | €893.85 | ||
Monthly attendance (tr)
An HR attendance sheet: Resource calendar variant="codes" with the Turkish code set (Ç, İ, R, HT, RT, Ü). The codes are data, not kit logic. Type a code to set a cell.
1–31 Ekim 2026
Accepted input formats
parseDuration reads hours, h:mm and unit words (h/min, sa/dk), with a decimal comma where the locale uses one. formatDuration has short, clock and decimal styles.
90 min · 1 h 30 min · 1:30 · 1.5 h
| 1.5 | 1 h 30 min |
| 1,5 | 1 h 30 min |
| 1:30 | 1 h 30 min |
| 90m | 1 h 30 min |
| 1h 30m | 1 h 30 min |
| 1 sa 30 dk | 1 h 30 min |
| 2 hours | 2 h |
| 45 | 45 h |
API
TimerButton
| Prop | Type | Default |
|---|---|---|
adapter{ getActive, start, stop, subscribe? }. createLocalTimerAdapter (localStorage + BroadcastChannel) or createMemoryTimerAdapter. Back it with your API for one timer per user. | TimerAdapter | — |
projectsProjects to choose from (full variant). | { id, label, meta? }[] | — |
projectId / defaultProjectId / onProjectChangeThe selected project. | string | — |
variantA top-bar pill, or project, note and a large counter. | "compact" | "full" | "compact" |
staleAfterMinAfter this many minutes a running timer asks “Still on it?”. | number | 600 |
onStart / onStopCalled after the adapter confirms. | (entry: TimeEntry) => void | — |
onAdjustAdds an “Adjust end time” button to a stale timer. | (entry) => void | — |
titleTickerPrefixes document.title with the elapsed time while the timer runs. | boolean | false |
labelsOverrides for the timeTracker namespace strings. | Partial<TimeTrackerLabels> | — |
Timesheet
| Prop | Type | Default |
|---|---|---|
weekStartFirst day shown. | string (YYYY-MM-DD) | — |
daysNumber of day columns. | number | 7 |
rowsProjects, tasks or people. | { id, label, meta?, readOnly? }[] | — |
value / defaultValueMinutes per cell. | Record<rowId, Record<YYYY-MM-DD, minutes>> | — |
onCellChangeSaves one cell. A rejected promise resets the cell and shows a toast. | (rowId, day, minutes) => void | Promise<void> | — |
dailyTargetDay totals under target on working days (up to today) turn amber and say so. | number (minutes) | — |
lockedAn approved week: read-only, with a lock note. | boolean | false |
workCalendarShades weekends and holidays. Half days halve the target. | WorkCalendar | from locale |
displayCell format: 7:30, 7.5 or 7 h 30 min. | "clock" | "decimal" | "short" | "clock" |
TimeEntryList
| Prop | Type | Default |
|---|---|---|
entries{ id, start, end?, durationMin?, projectLabel?, note?, billable?, rate? }. Running entries count up live. | TimeEntry[] | — |
onEntryOpenMakes each row a button. | (entry) => void | — |
showAmountsShows rate × time for billable entries. | boolean | false |
core (React-free)
| Prop | Type | Default |
|---|---|---|
parseDuration(input, locale)Minutes from “1.5”, “1,5”, “1:30”, “90m”, “1h 30m” or “1 sa 30 dk”. A bare number is hours. | number | null | — |
formatDuration(min, locale, style?)“3 h 20 min” / “3 sa 20 dk”, “3:20”, or decimal “3.33”. | string | — |
toLineItems(entries, groupBy) / lineItemsTotalBillable entries to { description, quantity, unitCode: "HUR", unitPrice, total, currency, entryIds }. | LineItemDraft[] / string | — |
timesheetTotals / entryMinutesRow, day and week sums, and the length of one entry. | functions | — |