Nasaq

TimeTracker

A running timer with project and task picker, manual time entries, an entries list grouped by day, and a day or week timesheet grid with totals.

PreviewOpen ↗

Code

import { TimeTracker, type TimeProject } from "@fadymondy/nasaq/web";const projects: TimeProject[] = [{ id: "web", name: "Website", tasks: [{ id: "ui", name: "UI polish" }] }];export function Timer() {  return <TimeTracker projects={projects} onStart={async (s) => api.start(s)} onStop={async (s) => api.stop(s)} />;}

Productivity · beta

Live examples and controls: TimeTracker in the lab.

Install

npx shadcn@latest add https://docs.nasaqui.com/r/time-tracker.json

Three pieces for logging work time. TimeTracker is the running timer: pick a project and task, add a note, start and stop. TimeEntryList lists entries by day with a manual entry dialog. Timesheet shows hours per project and task across a day or a week with row and column totals. Durations are whole seconds; the components are presentational and your callbacks save the data.

When to use

  • Freelancers, agencies and teams that log hours against projects and tasks.

When not to use

Import

import { TimeTracker, TimeEntryList, Timesheet } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"

Quick start

import { TimeTracker, type TimeProject } from "@fadymondy/nasaq/web";

const projects: TimeProject[] = [{ id: "web", name: "Website", tasks: [{ id: "ui", name: "UI polish" }] }];

export function Timer() {
  return <TimeTracker projects={projects} onStart={async (s) => api.start(s)} onStop={async (s) => api.stop(s)} />;
}

Anatomy

TimeTracker                     data-slot="time-tracker"
├─ timer readout                role="timer", h:mm:ss, project / task
├─ Start / Stop button
└─ project, task, note fields
TimeEntryList                   data-slot="time-entry-list"
├─ day cards                    total per day, entry rows with edit and delete
└─ TimeEntryDialog              date, project, task, duration, note
Timesheet                       data-slot="timesheet"
├─ view toggle and period navigation
└─ table                        rows by project and task, columns by day, totals

API

TimeTracker

PropTypeDefaultDescription
projectsreadonly TimeProject[]requiredid, name, tasks?: { id, name }[].
running?RunningTimer | nulluncontrolledControlled running timer.
defaultRunning?RunningTimer | nullnullInitial running timer (for example restored from your server).
onStart?(selection: TimerSelection & { startedAt: number }) => Promise<Result> | ResultnoneResolve { error } to stay stopped.
onStop?(stopped: StoppedTimer) => Promise<Result> | ResultnoneReceives elapsed seconds. Resolve { error } to keep running.
onRunningChange?(running: RunningTimer | null) => voidnoneEvery change of the running timer.
labels?TimeTrackerLabelsbuilt-in en/arString overrides.
className?stringnoneMerged onto the card.

The clock is derived from startedAt, so it stays right after a remount or a page reload.

TimeEntryList

PropTypeDefaultDescription
entriesreadonly TimeEntry[]requiredid, date (YYYY-MM-DD), seconds, projectId, taskId?, note?.
projectsreadonly TimeProject[]requiredFor names and pickers.
onAdd?(input: TimeEntryInput) => Promise<Result> | ResultnoneShows Add time and the manual entry dialog.
onEdit?(entry, input) => Promise<Result> | ResultnoneShows an edit button per entry.
onDelete?(entry: TimeEntry) => Promise<Result> | ResultnoneShows a delete button per entry.
labels?TimeTrackerLabelsbuilt-inString overrides.

TimeEntryDialog

Props: open, onOpenChange, projects, entry?, defaultDate?, onSubmit(input, entry?), labels?. Duration accepts 1:30, 1.5h, 90m, 2h 15m and Arabic-Indic digits, up to 24 hours.

Timesheet

PropTypeDefaultDescription
entriesreadonly TimeEntry[]requiredEntries to total.
projectsreadonly TimeProject[]requiredRow names.
view? / defaultView?TimesheetView ("day" | "week")"week"Controlled or initial view.
onViewChange?(view: TimesheetView) => voidnoneView change.
date? / defaultDate?DatetodayAny date inside the shown period.
onDateChange?(date: Date) => voidnonePeriod navigation.
weekStartsOn?number1, or 6 in Arabic0 Sunday to 6 Saturday.
labels?TimeTrackerLabelsbuilt-inString overrides.

Examples

Restore a running timer

<TimeTracker projects={projects} defaultRunning={{ projectId: "web", startedAt: Date.now() - 40 * 60 * 1000 }} />

Entries with manual add

<TimeEntryList entries={entries} projects={projects} onAdd={async (input) => api.create(input)} onDelete={async (e) => api.remove(e.id)} />

Weekly timesheet in Arabic

<NasaqProvider locale="ar">
  <Timesheet entries={entries} projects={projects} />
</NasaqProvider>

Accessibility

KeyAction
TabMove through fields and buttons.
EnterStart, stop or save.
Arrow keysChange the timesheet view and pick options.
EscapeClose the entry dialog.
  • The clock has role="timer" and does not announce each second; starting and stopping are announced through a status region.
  • The timesheet is a real table inside a labelled, focusable scroll region.
  • Localise labels for any custom copy.

RTL & i18n

Built-in English and Arabic. Clock and hour figures are isolated left to right. Dates use Intl. The week starts on Saturday by default in Arabic and previous and next arrows mirror.

Styling & tokens

Uses card, border, text and danger tokens. Target [data-slot="time-tracker"], [data-slot="time-entry-list"] and [data-slot="timesheet"]; extend with className. No raw hex.

Do / Don't

  • Do persist startedAt so the timer survives reloads.
  • Do keep entries in whole seconds.
  • Don't rely on colour alone for the running state; the button changes to Stop.

Lab

https://docs.nasaqui.com/?path=/docs/components-productivity-timetracker--docs

On this page