super-time-tracker-ui 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +28 -0
  3. package/CLAUDE.md +1 -0
  4. package/README.md +36 -0
  5. package/app/api/backup/route.ts +39 -0
  6. package/app/api/entry/delete-bulk/route.ts +53 -0
  7. package/app/api/entry/move/route.ts +46 -0
  8. package/app/api/entry/move-bulk/route.ts +62 -0
  9. package/app/api/entry/route.ts +75 -0
  10. package/app/api/in/route.ts +38 -0
  11. package/app/api/note/route.ts +120 -0
  12. package/app/api/out/route.ts +31 -0
  13. package/app/api/sheet/route.ts +68 -0
  14. package/app/api/state/route.ts +16 -0
  15. package/app/api/tags/route.ts +75 -0
  16. package/app/color-palettes.css +260 -0
  17. package/app/favicon.ico +0 -0
  18. package/app/globals.css +140 -0
  19. package/app/layout.tsx +54 -0
  20. package/app/page.tsx +24 -0
  21. package/app/reporting/page.tsx +11 -0
  22. package/app/settings/data/page.tsx +9 -0
  23. package/app/settings/display/page.tsx +8 -0
  24. package/app/settings/page.tsx +12 -0
  25. package/app/settings/tags/page.tsx +13 -0
  26. package/bin/stt-ui.js +63 -0
  27. package/components/active-entry-panel.tsx +199 -0
  28. package/components/backup-restore-setting.tsx +168 -0
  29. package/components/check-in-form-collapsed-setting.tsx +44 -0
  30. package/components/check-in-form-collapsible.tsx +52 -0
  31. package/components/check-in-form.tsx +89 -0
  32. package/components/checkbox.tsx +75 -0
  33. package/components/checkout-button-group.tsx +73 -0
  34. package/components/chevron-icon.tsx +25 -0
  35. package/components/clear-tag-filters-on-sheet-change-setting.tsx +45 -0
  36. package/components/color-palette-setting.tsx +75 -0
  37. package/components/compact-lists-setting.tsx +42 -0
  38. package/components/confirm-before-checkout-setting.tsx +42 -0
  39. package/components/confirm-destructive-actions-setting.tsx +46 -0
  40. package/components/confirm-dialog-provider.tsx +71 -0
  41. package/components/confirm-dialog.tsx +90 -0
  42. package/components/data-settings-view.tsx +47 -0
  43. package/components/default-reporting-range-setting.tsx +56 -0
  44. package/components/default-reporting-sort-setting.tsx +45 -0
  45. package/components/default-sheet-session-setting.tsx +118 -0
  46. package/components/display-settings-view.tsx +75 -0
  47. package/components/duration-format-setting.tsx +40 -0
  48. package/components/entry-actions-menu.tsx +207 -0
  49. package/components/entry-edit-form.tsx +113 -0
  50. package/components/entry-list-bulk-bar.tsx +128 -0
  51. package/components/entry-list-sort-setting.tsx +41 -0
  52. package/components/entry-list.tsx +336 -0
  53. package/components/entry-notes-list.tsx +211 -0
  54. package/components/entry-tag-filter.tsx +99 -0
  55. package/components/format_datetime_hint.ts +8 -0
  56. package/components/format_time.ts +10 -0
  57. package/components/general-settings-view.tsx +40 -0
  58. package/components/hamburger-icon.tsx +21 -0
  59. package/components/note-edit-form.tsx +77 -0
  60. package/components/note-form.tsx +109 -0
  61. package/components/pencil-icon.tsx +21 -0
  62. package/components/reporting-date-range-picker.tsx +121 -0
  63. package/components/reporting-sort-controls.tsx +53 -0
  64. package/components/reporting-view.tsx +340 -0
  65. package/components/setting-radio-group.tsx +79 -0
  66. package/components/settings-nav.tsx +66 -0
  67. package/components/settings-page-layout.tsx +53 -0
  68. package/components/settings-saved-toast.tsx +57 -0
  69. package/components/sheet-actions-menu.tsx +108 -0
  70. package/components/sheet-sidebar.tsx +196 -0
  71. package/components/tag-autocomplete-input.tsx +183 -0
  72. package/components/tag-filter-mode-setting.tsx +47 -0
  73. package/components/tag-management-view.tsx +290 -0
  74. package/components/theme-mode-setting.tsx +44 -0
  75. package/components/theme-mode-system-listener.tsx +43 -0
  76. package/components/theme_switcher.tsx +38 -0
  77. package/components/time-format-setting.tsx +39 -0
  78. package/components/timer-in-title-setting.tsx +38 -0
  79. package/components/timer-show-seconds-setting.tsx +41 -0
  80. package/components/tracker-active-bar.tsx +76 -0
  81. package/components/tracker-app.tsx +338 -0
  82. package/components/tracker-breadcrumb.tsx +56 -0
  83. package/components/tracker-document-title.tsx +67 -0
  84. package/components/tracker-topbar.tsx +63 -0
  85. package/components/trash-icon.tsx +24 -0
  86. package/components/week-starts-on-setting.tsx +39 -0
  87. package/eslint.config.mjs +18 -0
  88. package/lib/add_note_to_entry.ts +65 -0
  89. package/lib/api_error_response.ts +10 -0
  90. package/lib/apply_accent_color.ts +12 -0
  91. package/lib/apply_color_palette.ts +12 -0
  92. package/lib/apply_compact_lists.ts +9 -0
  93. package/lib/apply_tag_autocomplete_selection.ts +26 -0
  94. package/lib/apply_theme.ts +8 -0
  95. package/lib/build_reporting_stats.ts +55 -0
  96. package/lib/build_resume_description.ts +15 -0
  97. package/lib/check_in_entry.ts +81 -0
  98. package/lib/check_out_entry.ts +75 -0
  99. package/lib/collect_known_tags.ts +22 -0
  100. package/lib/collect_tag_stats.ts +27 -0
  101. package/lib/collect_tags_from_entries.ts +35 -0
  102. package/lib/config.ts +9 -0
  103. package/lib/convert_json_db.ts +49 -0
  104. package/lib/delete_entries.ts +62 -0
  105. package/lib/delete_entry.ts +29 -0
  106. package/lib/delete_note_on_entry.ts +42 -0
  107. package/lib/delete_sheet.ts +30 -0
  108. package/lib/delete_tracker_action.ts +22 -0
  109. package/lib/edit_entry.ts +56 -0
  110. package/lib/edit_note_on_entry.ts +49 -0
  111. package/lib/ensure_dir_exists.ts +22 -0
  112. package/lib/entry_matches_tag_filter.ts +26 -0
  113. package/lib/fetch_tracker_state.ts +15 -0
  114. package/lib/filter_entries_by_tags.ts +20 -0
  115. package/lib/filter_known_tags.ts +20 -0
  116. package/lib/find_all_serialized_active_entries.ts +28 -0
  117. package/lib/find_serialized_active_entry.ts +12 -0
  118. package/lib/find_serialized_active_entry_for_sheet.ts +31 -0
  119. package/lib/find_sheet_with_active_entry.ts +16 -0
  120. package/lib/format_display_tag.ts +6 -0
  121. package/lib/format_duration.ts +45 -0
  122. package/lib/gen_db.ts +43 -0
  123. package/lib/get_active_panel_class_name.ts +20 -0
  124. package/lib/get_average_entry_ms.ts +13 -0
  125. package/lib/get_button_class_name.ts +24 -0
  126. package/lib/get_check_out_confirm_dialog.ts +19 -0
  127. package/lib/get_clipped_entry_duration_ms.ts +18 -0
  128. package/lib/get_compact_lists_snapshot.ts +15 -0
  129. package/lib/get_date_range_ms_from_inputs.ts +31 -0
  130. package/lib/get_delete_entries_confirm_dialog.ts +21 -0
  131. package/lib/get_delete_entry_confirm_dialog.ts +19 -0
  132. package/lib/get_delete_note_confirm_dialog.ts +21 -0
  133. package/lib/get_delete_sheet_confirm_dialog.ts +25 -0
  134. package/lib/get_entry_duration_ms.ts +14 -0
  135. package/lib/get_entry_row_key.ts +8 -0
  136. package/lib/get_initial_preferred_sheet_name.ts +34 -0
  137. package/lib/get_initial_reporting_range_inputs.ts +31 -0
  138. package/lib/get_input_class_name.ts +15 -0
  139. package/lib/get_merge_tags_confirm_dialog.ts +25 -0
  140. package/lib/get_period_range_ms.ts +43 -0
  141. package/lib/get_reporting_date_range_shortcut_inputs.ts +84 -0
  142. package/lib/get_reporting_period_totals.ts +39 -0
  143. package/lib/get_reporting_stats.ts +25 -0
  144. package/lib/get_restore_db_confirm_dialog.ts +14 -0
  145. package/lib/get_running_entry_key.ts +8 -0
  146. package/lib/get_serialized_entries_total_ms.ts +10 -0
  147. package/lib/get_sheet.ts +14 -0
  148. package/lib/get_sheet_report_stats.ts +22 -0
  149. package/lib/get_sheet_report_stats_for_range.ts +46 -0
  150. package/lib/get_sheet_tag_filter_snapshot.ts +22 -0
  151. package/lib/get_sheets_duration_in_range.ts +27 -0
  152. package/lib/get_tag_autocomplete_context.ts +32 -0
  153. package/lib/get_theme_snapshot.ts +16 -0
  154. package/lib/get_tracker_state.ts +67 -0
  155. package/lib/has_string_value.ts +6 -0
  156. package/lib/is_entry_in_day.ts +15 -0
  157. package/lib/is_idle_sheet_report.ts +8 -0
  158. package/lib/is_json_time_tracker_db.ts +14 -0
  159. package/lib/merge_tags_across_db.ts +79 -0
  160. package/lib/migrate_json_db.ts +56 -0
  161. package/lib/migrate_json_db_to_version_three.ts +51 -0
  162. package/lib/migrate_json_db_to_version_two.ts +50 -0
  163. package/lib/move_entries_to_sheet.ts +152 -0
  164. package/lib/move_entry_to_sheet.ts +82 -0
  165. package/lib/normalize_stored_tag.ts +16 -0
  166. package/lib/notify_settings_saved.ts +47 -0
  167. package/lib/parse_default_sheet_session_mode.ts +21 -0
  168. package/lib/parse_entry_from_input.ts +23 -0
  169. package/lib/parse_natural_language_date.ts +23 -0
  170. package/lib/parse_reporting_source_sheets.ts +22 -0
  171. package/lib/partition_sheet_report_stats.ts +30 -0
  172. package/lib/patch_tracker_action.ts +22 -0
  173. package/lib/persist_ui_preference.ts +18 -0
  174. package/lib/post_tracker_action.ts +22 -0
  175. package/lib/preferences/accent_color_preference.ts +21 -0
  176. package/lib/preferences/check_in_form_collapsed_preference.ts +20 -0
  177. package/lib/preferences/clear_tag_filters_on_sheet_change_preference.ts +20 -0
  178. package/lib/preferences/color_palette_preference.ts +21 -0
  179. package/lib/preferences/confirm_before_checkout_preference.ts +20 -0
  180. package/lib/preferences/confirm_destructive_actions_preference.ts +20 -0
  181. package/lib/preferences/default_reporting_range_preference.ts +21 -0
  182. package/lib/preferences/default_reporting_sort_preference.ts +24 -0
  183. package/lib/preferences/duration_format_preference.ts +19 -0
  184. package/lib/preferences/entry_list_sort_preference.ts +21 -0
  185. package/lib/preferences/tag_filter_mode_preference.ts +18 -0
  186. package/lib/preferences/theme_mode_preference.ts +18 -0
  187. package/lib/preferences/time_format_preference.ts +18 -0
  188. package/lib/preferences/timer_in_title_preference.ts +18 -0
  189. package/lib/preferences/timer_show_seconds_preference.ts +19 -0
  190. package/lib/preferences/week_starts_on_preference.ts +19 -0
  191. package/lib/prompt_check_out_at.ts +17 -0
  192. package/lib/prompt_entry_note.ts +14 -0
  193. package/lib/prune_sheet_tag_filter.ts +27 -0
  194. package/lib/read_db.ts +49 -0
  195. package/lib/read_db_backup_contents.ts +22 -0
  196. package/lib/read_document_compact_lists.ts +12 -0
  197. package/lib/read_document_theme.ts +14 -0
  198. package/lib/read_sheet_tag_filter.ts +26 -0
  199. package/lib/read_stored_active_sheet.ts +14 -0
  200. package/lib/read_stored_compact_lists.ts +24 -0
  201. package/lib/read_stored_default_sheet_fixed_name.ts +16 -0
  202. package/lib/read_stored_default_sheet_session_mode.ts +18 -0
  203. package/lib/read_stored_sheet_tag_filters.ts +28 -0
  204. package/lib/read_stored_theme.ts +18 -0
  205. package/lib/rename_sheet.ts +39 -0
  206. package/lib/rename_tag_across_db.ts +19 -0
  207. package/lib/resolve_active_sheet_name.ts +36 -0
  208. package/lib/resolve_session_preferred_sheet.ts +37 -0
  209. package/lib/resolve_theme.ts +18 -0
  210. package/lib/resolve_theme_mode_to_theme.ts +19 -0
  211. package/lib/restore_db_from_uploaded_json.ts +24 -0
  212. package/lib/serialize_entry.ts +27 -0
  213. package/lib/serialize_reporting_source_sheets.ts +19 -0
  214. package/lib/serialize_sheet_entries.ts +18 -0
  215. package/lib/set_accent_color.ts +12 -0
  216. package/lib/set_active_sheet.ts +18 -0
  217. package/lib/set_color_palette.ts +12 -0
  218. package/lib/set_compact_lists.ts +12 -0
  219. package/lib/set_default_sheet_fixed_name.ts +8 -0
  220. package/lib/set_default_sheet_session_mode.ts +11 -0
  221. package/lib/set_sheet_tag_filter.ts +13 -0
  222. package/lib/set_theme_mode.ts +19 -0
  223. package/lib/sheet_tag_filter_snapshots.ts +48 -0
  224. package/lib/sort_serialized_entries.ts +35 -0
  225. package/lib/sort_sheet_report_stats.ts +43 -0
  226. package/lib/subscribe_compact_lists.ts +25 -0
  227. package/lib/subscribe_sheet_tag_filters.ts +28 -0
  228. package/lib/subscribe_theme.ts +23 -0
  229. package/lib/sync_active_sheet_preference.ts +19 -0
  230. package/lib/tags_are_equal.ts +12 -0
  231. package/lib/theme_init_script.ts +11 -0
  232. package/lib/toggle_sheet_tag_filter.ts +28 -0
  233. package/lib/toggle_theme.ts +20 -0
  234. package/lib/types/confirm_dialog.ts +9 -0
  235. package/lib/types/data.ts +16 -0
  236. package/lib/types/generic_data.ts +25 -0
  237. package/lib/types/index.ts +2 -0
  238. package/lib/types/reporting.ts +59 -0
  239. package/lib/types/tag_management.ts +7 -0
  240. package/lib/types/theme.ts +3 -0
  241. package/lib/types/tracker_state.ts +39 -0
  242. package/lib/types/ui_preferences.ts +104 -0
  243. package/lib/types/ui_settings.ts +17 -0
  244. package/lib/ui_preference_store.ts +80 -0
  245. package/lib/ui_settings_init_script.ts +33 -0
  246. package/lib/use_check_in_form_collapsed.ts +18 -0
  247. package/lib/use_clear_tag_filters_on_sheet_change.ts +18 -0
  248. package/lib/use_confirm_before_checkout.ts +18 -0
  249. package/lib/use_confirm_destructive_actions.ts +18 -0
  250. package/lib/use_duration_format.ts +17 -0
  251. package/lib/use_entry_list_sort.ts +17 -0
  252. package/lib/use_tag_filter_mode.ts +17 -0
  253. package/lib/use_time_format.ts +17 -0
  254. package/lib/use_timer_in_title.ts +18 -0
  255. package/lib/use_timer_show_seconds.ts +18 -0
  256. package/lib/use_week_starts_on.ts +17 -0
  257. package/lib/validate_entry_times.ts +12 -0
  258. package/lib/week_starts_on_to_index.ts +8 -0
  259. package/lib/write_active_sheet_preference.ts +28 -0
  260. package/lib/write_db.ts +20 -0
  261. package/lib/write_sheet_tag_filter.ts +20 -0
  262. package/lib/write_stored_compact_lists.ts +15 -0
  263. package/lib/write_stored_default_sheet_fixed_name.ts +28 -0
  264. package/lib/write_stored_default_sheet_session_mode.ts +24 -0
  265. package/lib/write_stored_sheet_tag_filters.ts +18 -0
  266. package/lib/write_stored_theme.ts +12 -0
  267. package/next.config.ts +7 -0
  268. package/package.json +96 -0
  269. package/pnpm-workspace.yaml +7 -0
  270. package/postcss.config.mjs +7 -0
  271. package/public/file.svg +1 -0
  272. package/public/globe.svg +1 -0
  273. package/public/next.svg +1 -0
  274. package/public/vercel.svg +1 -0
  275. package/public/window.svg +1 -0
  276. package/tsconfig.json +34 -0
@@ -0,0 +1,12 @@
1
+ import { type AccentColor } from '@/lib/types/ui_preferences'
2
+
3
+ /**
4
+ * Sets the active accent color on the document element.
5
+ */
6
+ export function apply_accent_color(value: AccentColor): void {
7
+ if (typeof document === 'undefined') {
8
+ return
9
+ }
10
+
11
+ document.documentElement.setAttribute('data-accent', value)
12
+ }
@@ -0,0 +1,12 @@
1
+ import { type ColorPalette } from '@/lib/types/ui_preferences'
2
+
3
+ /**
4
+ * Sets the active color palette on the document element.
5
+ */
6
+ export function apply_color_palette(value: ColorPalette): void {
7
+ if (typeof document === 'undefined') {
8
+ return
9
+ }
10
+
11
+ document.documentElement.setAttribute('data-palette', value)
12
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Applies the compact lists setting to the document root element.
3
+ */
4
+ export function apply_compact_lists(enabled: boolean): void {
5
+ document.documentElement.setAttribute(
6
+ "data-compact-lists",
7
+ enabled ? "true" : "false",
8
+ );
9
+ }
@@ -0,0 +1,26 @@
1
+ import { format_display_tag } from '@/lib/format_display_tag'
2
+ import { type TagAutocompleteContext } from '@/lib/get_tag_autocomplete_context'
3
+
4
+ export interface TagAutocompleteSelectionResult {
5
+ next_text: string
6
+ next_cursor: number
7
+ }
8
+
9
+ /**
10
+ * Inserts a selected tag into the description at the active @ token.
11
+ */
12
+ export function apply_tag_autocomplete_selection(
13
+ text: string,
14
+ context: TagAutocompleteContext,
15
+ tag: string,
16
+ ): TagAutocompleteSelectionResult {
17
+ const formatted_tag = format_display_tag(tag)
18
+ const before = text.slice(0, context.start_index)
19
+ const after = text.slice(context.end_index)
20
+ const next_text = `${before}${formatted_tag}${after}`
21
+
22
+ return {
23
+ next_text,
24
+ next_cursor: before.length + formatted_tag.length,
25
+ }
26
+ }
@@ -0,0 +1,8 @@
1
+ import { type Theme } from "@/lib/types/theme";
2
+
3
+ /**
4
+ * Applies the theme to the document root element.
5
+ */
6
+ export function apply_theme(theme: Theme): void {
7
+ document.documentElement.setAttribute("data-theme", theme);
8
+ }
@@ -0,0 +1,55 @@
1
+ import { get_average_entry_ms } from '@/lib/get_average_entry_ms'
2
+ import { type PeriodRangeMs } from '@/lib/get_period_range_ms'
3
+ import { get_reporting_period_totals } from '@/lib/get_reporting_period_totals'
4
+ import { get_sheet_report_stats } from '@/lib/get_sheet_report_stats'
5
+ import { get_sheet_report_stats_for_range } from '@/lib/get_sheet_report_stats_for_range'
6
+ import { partition_sheet_report_stats } from '@/lib/partition_sheet_report_stats'
7
+ import { type ReportingStats } from '@/lib/types/reporting'
8
+ import { type TimeSheet } from '@/lib/types'
9
+
10
+ /**
11
+ * Builds a full reporting snapshot, optionally clipped to a date range.
12
+ */
13
+ export function build_reporting_stats(
14
+ sheets: TimeSheet[],
15
+ range: PeriodRangeMs | null = null,
16
+ now: number = Date.now(),
17
+ week_starts_on: 0 | 1 = 1,
18
+ ): ReportingStats {
19
+ const sheet_stats = sheets.map((sheet) =>
20
+ range === null
21
+ ? get_sheet_report_stats(sheet)
22
+ : get_sheet_report_stats_for_range(
23
+ sheet,
24
+ range.startMs,
25
+ range.endMs,
26
+ now,
27
+ ),
28
+ )
29
+ const { activeSheets, idleSheets } = partition_sheet_report_stats(sheet_stats)
30
+ const grand_total_ms = sheet_stats.reduce(
31
+ (total, sheet) => total + sheet.totalMs,
32
+ 0,
33
+ )
34
+ const total_entry_count = sheet_stats.reduce(
35
+ (total, sheet) => total + sheet.entryCount,
36
+ 0,
37
+ )
38
+
39
+ return {
40
+ activeSheets,
41
+ idleSheets,
42
+ grandTotalMs: grand_total_ms,
43
+ totalEntryCount: total_entry_count,
44
+ grandAverageEntryMs: get_average_entry_ms(grand_total_ms, total_entry_count),
45
+ periodTotals:
46
+ range === null
47
+ ? get_reporting_period_totals(
48
+ sheets,
49
+ new Date(now),
50
+ now,
51
+ week_starts_on,
52
+ )
53
+ : { todayMs: 0, weekMs: 0, monthMs: 0 },
54
+ }
55
+ }
@@ -0,0 +1,15 @@
1
+ import { format_display_tag } from "@/lib/format_display_tag";
2
+
3
+ /**
4
+ * Builds a check-in description from a prior entry's text and tags.
5
+ */
6
+ export function build_resume_description(
7
+ description: string,
8
+ tags: string[],
9
+ ): string {
10
+ const trimmed = description.trim();
11
+ const base = trimmed.length > 0 ? trimmed : "Untitled entry";
12
+ const tag_text = tags.map((tag) => format_display_tag(tag)).join(" ");
13
+
14
+ return tag_text.length > 0 ? `${base} ${tag_text}`.trim() : base;
15
+ }
@@ -0,0 +1,81 @@
1
+ import { gen_sheet } from "@/lib/gen_db";
2
+ import { get_sheet } from "@/lib/get_sheet";
3
+ import { parse_entry_from_input } from "@/lib/parse_entry_from_input";
4
+ import { parse_natural_language_date } from "@/lib/parse_natural_language_date";
5
+ import { read_db } from "@/lib/read_db";
6
+ import { write_db } from "@/lib/write_db";
7
+ import { type TimeSheetEntry, type TimeTrackerDB } from "@/lib/types";
8
+
9
+ export interface CheckInEntryArgs {
10
+ description: string;
11
+ sheet_name?: string;
12
+ note?: string;
13
+ at?: string;
14
+ }
15
+
16
+ /**
17
+ * Starts a new active entry on the requested or active sheet.
18
+ */
19
+ export async function check_in_entry(
20
+ args: CheckInEntryArgs,
21
+ ): Promise<TimeSheetEntry> {
22
+ const { at, description, note, sheet_name: input_sheet_name } = args;
23
+ const db = await read_db();
24
+ const active_sheet_name = db.activeSheetName;
25
+ const sheet_name =
26
+ input_sheet_name === undefined || input_sheet_name.length === 0
27
+ ? active_sheet_name
28
+ : input_sheet_name;
29
+
30
+ if (sheet_name === null) {
31
+ throw new Error("No active sheet");
32
+ }
33
+
34
+ const sheet_exists = db.sheets.some(({ name }) => name === sheet_name);
35
+ const sheet = sheet_exists
36
+ ? get_sheet(db, sheet_name)
37
+ : add_sheet_to_db(db, sheet_name);
38
+
39
+ if (sheet.activeEntryID !== null) {
40
+ const entry = sheet.entries.find(({ id }) => id === sheet.activeEntryID);
41
+
42
+ if (entry !== undefined) {
43
+ throw new Error(
44
+ `An entry is already active (${entry.id}): ${entry.description}`,
45
+ );
46
+ }
47
+
48
+ sheet.activeEntryID = null;
49
+ }
50
+
51
+ const start_date =
52
+ at === undefined || at.trim().length === 0
53
+ ? new Date()
54
+ : parse_natural_language_date(at);
55
+
56
+ const id = sheet.entries.length;
57
+ const parsed = parse_entry_from_input(id, description, start_date);
58
+ const entry: TimeSheetEntry = {
59
+ ...parsed,
60
+ description:
61
+ parsed.description.length > 0 ? parsed.description : description.trim(),
62
+ };
63
+
64
+ if (note !== undefined && note.length > 0) {
65
+ entry.notes.push({ timestamp: new Date(), text: note });
66
+ }
67
+
68
+ sheet.entries.push(entry);
69
+ sheet.activeEntryID = id;
70
+ db.activeSheetName = sheet.name;
71
+
72
+ await write_db(db);
73
+
74
+ return entry;
75
+ }
76
+
77
+ function add_sheet_to_db(db: TimeTrackerDB, sheet_name: string) {
78
+ const sheet = gen_sheet(sheet_name);
79
+ db.sheets.push(sheet);
80
+ return sheet;
81
+ }
@@ -0,0 +1,75 @@
1
+ import { find_sheet_with_active_entry } from "@/lib/find_sheet_with_active_entry";
2
+ import { get_sheet } from "@/lib/get_sheet";
3
+ import { parse_natural_language_date } from "@/lib/parse_natural_language_date";
4
+ import { read_db } from "@/lib/read_db";
5
+ import { validate_entry_times } from "@/lib/validate_entry_times";
6
+ import { write_db } from "@/lib/write_db";
7
+ import { type TimeSheetEntry } from "@/lib/types";
8
+
9
+ export interface CheckOutEntryArgs {
10
+ sheet_name?: string;
11
+ note?: string;
12
+ at?: string;
13
+ }
14
+
15
+ /**
16
+ * Checks out of the active entry on the requested or active sheet.
17
+ */
18
+ export async function check_out_entry(
19
+ args: CheckOutEntryArgs = {},
20
+ ): Promise<TimeSheetEntry> {
21
+ const { at, note, sheet_name: input_sheet_name } = args;
22
+ const db = await read_db();
23
+ const sheet = resolve_check_out_sheet(db, input_sheet_name);
24
+ const { activeEntryID, name: sheet_name } = sheet;
25
+
26
+ if (activeEntryID === null) {
27
+ throw new Error(`No active entry for sheet ${sheet_name}`);
28
+ }
29
+
30
+ const entry = sheet.entries.find(({ id }) => id === activeEntryID);
31
+
32
+ if (entry === undefined) {
33
+ throw new Error(`No entry found with ID ${activeEntryID}`);
34
+ }
35
+
36
+ const end_date =
37
+ at === undefined || at.trim().length === 0
38
+ ? new Date()
39
+ : parse_natural_language_date(at);
40
+
41
+ entry.end = end_date;
42
+ sheet.activeEntryID = null;
43
+ validate_entry_times(entry.start, entry.end);
44
+
45
+ if (note !== undefined && note.length > 0) {
46
+ entry.notes.push({ timestamp: new Date(), text: note });
47
+ }
48
+
49
+ await write_db(db);
50
+
51
+ return entry;
52
+ }
53
+
54
+ function resolve_check_out_sheet(
55
+ db: Awaited<ReturnType<typeof read_db>>,
56
+ input_sheet_name: string | undefined,
57
+ ) {
58
+ if (input_sheet_name !== undefined && input_sheet_name.length > 0) {
59
+ return get_sheet(db, input_sheet_name);
60
+ }
61
+
62
+ const sheet_with_running_entry = find_sheet_with_active_entry(db);
63
+
64
+ if (sheet_with_running_entry !== null) {
65
+ return sheet_with_running_entry;
66
+ }
67
+
68
+ const active_sheet_name = db.activeSheetName;
69
+
70
+ if (active_sheet_name === null) {
71
+ throw new Error("No active sheet");
72
+ }
73
+
74
+ return get_sheet(db, active_sheet_name);
75
+ }
@@ -0,0 +1,22 @@
1
+ import { type TimeTrackerDB } from '@/lib/types'
2
+
3
+ /**
4
+ * Collects unique tags from every entry in the database.
5
+ */
6
+ export function collect_known_tags(db: TimeTrackerDB): string[] {
7
+ const tags = new Set<string>()
8
+
9
+ for (const sheet of db.sheets) {
10
+ for (const entry of sheet.entries) {
11
+ for (const tag of entry.tags) {
12
+ const trimmed = tag.trim()
13
+
14
+ if (trimmed.length > 0) {
15
+ tags.add(trimmed)
16
+ }
17
+ }
18
+ }
19
+ }
20
+
21
+ return [...tags].sort((left, right) => left.localeCompare(right))
22
+ }
@@ -0,0 +1,27 @@
1
+ import { normalize_stored_tag } from '@/lib/normalize_stored_tag'
2
+ import { type TagStat } from '@/lib/types/tag_management'
3
+ import { type TimeTrackerDB } from '@/lib/types'
4
+
5
+ /**
6
+ * Builds tag usage counts from every entry in the database.
7
+ */
8
+ export function collect_tag_stats(db: TimeTrackerDB): TagStat[] {
9
+ const counts = new Map<string, number>()
10
+
11
+ for (const sheet of db.sheets) {
12
+ for (const entry of sheet.entries) {
13
+ for (const tag of entry.tags) {
14
+ try {
15
+ const normalized = normalize_stored_tag(tag)
16
+ counts.set(normalized, (counts.get(normalized) ?? 0) + 1)
17
+ } catch {
18
+ continue
19
+ }
20
+ }
21
+ }
22
+ }
23
+
24
+ return [...counts.entries()]
25
+ .map(([name, entryCount]) => ({ name, entryCount }))
26
+ .sort((left, right) => left.name.localeCompare(right.name))
27
+ }
@@ -0,0 +1,35 @@
1
+ import { normalize_stored_tag } from '@/lib/normalize_stored_tag'
2
+ import { type TagStat } from '@/lib/types/tag_management'
3
+ import { type SerializedEntry } from '@/lib/types/tracker_state'
4
+
5
+ /**
6
+ * Builds tag usage counts from a list of serialized entries.
7
+ */
8
+ export function collect_tags_from_entries(
9
+ entries: SerializedEntry[],
10
+ ): TagStat[] {
11
+ const counts = new Map<string, number>()
12
+
13
+ for (const entry of entries) {
14
+ const seen_on_entry = new Set<string>()
15
+
16
+ for (const tag of entry.tags) {
17
+ try {
18
+ const normalized = normalize_stored_tag(tag)
19
+
20
+ if (seen_on_entry.has(normalized)) {
21
+ continue
22
+ }
23
+
24
+ seen_on_entry.add(normalized)
25
+ counts.set(normalized, (counts.get(normalized) ?? 0) + 1)
26
+ } catch {
27
+ continue
28
+ }
29
+ }
30
+ }
31
+
32
+ return [...counts.entries()]
33
+ .map(([name, entryCount]) => ({ name, entryCount }))
34
+ .sort((left, right) => left.name.localeCompare(right.name))
35
+ }
package/lib/config.ts ADDED
@@ -0,0 +1,9 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+
4
+ export const DB_FILE_NAME = "db.json";
5
+ export const DEFAULT_SHEET_NAME = "main";
6
+ export const STORAGE_DIR_NAME = ".super-time-tracker";
7
+ export const STORAGE_PATH = path.join(os.homedir(), STORAGE_DIR_NAME);
8
+ export const DB_PATH = path.join(STORAGE_PATH, DB_FILE_NAME);
9
+ export const DB_VERSION = 3;
@@ -0,0 +1,49 @@
1
+ import { type JSONTimeTrackerDB, type TimeTrackerDB } from "@/lib/types";
2
+
3
+ /**
4
+ * Converts a JSON-serialized DB into in-memory Date instances.
5
+ */
6
+ export function convert_json_db(json_db: JSONTimeTrackerDB): TimeTrackerDB {
7
+ const {
8
+ sheets: json_sheets,
9
+ version: json_version,
10
+ activeSheetName: json_active_sheet_name,
11
+ } = json_db;
12
+
13
+ const sheets = json_sheets.map(
14
+ ({
15
+ name: json_name,
16
+ entries: json_entries,
17
+ activeEntryID: json_active_entry_id,
18
+ }) => ({
19
+ name: json_name,
20
+ activeEntryID: json_active_entry_id,
21
+ entries: json_entries.map(
22
+ ({
23
+ id: json_id,
24
+ start: json_start,
25
+ end: json_end,
26
+ description: json_description,
27
+ tags: json_tags,
28
+ notes: json_notes,
29
+ }) => ({
30
+ id: json_id,
31
+ description: json_description,
32
+ tags: json_tags,
33
+ notes: json_notes.map(({ timestamp, text }) => ({
34
+ text,
35
+ timestamp: new Date(timestamp),
36
+ })),
37
+ start: new Date(json_start),
38
+ end: json_end === null ? null : new Date(json_end),
39
+ }),
40
+ ),
41
+ }),
42
+ );
43
+
44
+ return {
45
+ version: json_version,
46
+ activeSheetName: json_active_sheet_name,
47
+ sheets,
48
+ };
49
+ }
@@ -0,0 +1,62 @@
1
+ import { get_sheet } from '@/lib/get_sheet'
2
+ import { read_db } from '@/lib/read_db'
3
+ import { write_db } from '@/lib/write_db'
4
+
5
+ export interface DeleteEntryRef {
6
+ sheet_name: string
7
+ entry_id: number
8
+ }
9
+
10
+ export interface DeleteEntriesArgs {
11
+ entries: DeleteEntryRef[]
12
+ }
13
+
14
+ /**
15
+ * Deletes multiple entries across sheets in a single database write.
16
+ */
17
+ export async function delete_entries(args: DeleteEntriesArgs): Promise<void> {
18
+ const { entries } = args
19
+
20
+ if (entries.length === 0) {
21
+ throw new Error('No entries selected')
22
+ }
23
+
24
+ const db = await read_db()
25
+ const seen = new Set<string>()
26
+ const by_sheet = new Map<string, number[]>()
27
+
28
+ for (const { entry_id, sheet_name } of entries) {
29
+ const key = `${sheet_name}:${entry_id}`
30
+
31
+ if (seen.has(key)) {
32
+ continue
33
+ }
34
+
35
+ seen.add(key)
36
+
37
+ const list = by_sheet.get(sheet_name) ?? []
38
+ list.push(entry_id)
39
+ by_sheet.set(sheet_name, list)
40
+ }
41
+
42
+ for (const [sheet_name, ids] of by_sheet) {
43
+ const sheet = get_sheet(db, sheet_name)
44
+ const ids_to_remove = [...ids].sort((left, right) => right - left)
45
+
46
+ for (const entry_id of ids_to_remove) {
47
+ const entry_index = sheet.entries.findIndex(({ id }) => id === entry_id)
48
+
49
+ if (entry_index === -1) {
50
+ continue
51
+ }
52
+
53
+ if (sheet.activeEntryID === entry_id) {
54
+ sheet.activeEntryID = null
55
+ }
56
+
57
+ sheet.entries.splice(entry_index, 1)
58
+ }
59
+ }
60
+
61
+ await write_db(db)
62
+ }
@@ -0,0 +1,29 @@
1
+ import { get_sheet } from "@/lib/get_sheet";
2
+ import { read_db } from "@/lib/read_db";
3
+ import { write_db } from "@/lib/write_db";
4
+
5
+ export interface DeleteEntryArgs {
6
+ sheet_name: string;
7
+ entry_id: number;
8
+ }
9
+
10
+ /**
11
+ * Deletes an entry from a sheet.
12
+ */
13
+ export async function delete_entry(args: DeleteEntryArgs): Promise<void> {
14
+ const { entry_id, sheet_name } = args;
15
+ const db = await read_db();
16
+ const sheet = get_sheet(db, sheet_name);
17
+ const entry_index = sheet.entries.findIndex(({ id }) => id === entry_id);
18
+
19
+ if (entry_index === -1) {
20
+ throw new Error(`Entry ${entry_id} not found in sheet ${sheet_name}`);
21
+ }
22
+
23
+ if (sheet.activeEntryID === entry_id) {
24
+ sheet.activeEntryID = null;
25
+ }
26
+
27
+ sheet.entries.splice(entry_index, 1);
28
+ await write_db(db);
29
+ }
@@ -0,0 +1,42 @@
1
+ import { get_sheet } from '@/lib/get_sheet'
2
+ import { read_db } from '@/lib/read_db'
3
+ import { write_db } from '@/lib/write_db'
4
+
5
+ export interface DeleteNoteOnEntryArgs {
6
+ note_timestamp: string
7
+ sheet_name: string
8
+ entry_id: number
9
+ }
10
+
11
+ /**
12
+ * Removes a note from a time sheet entry by timestamp.
13
+ */
14
+ export async function delete_note_on_entry(
15
+ args: DeleteNoteOnEntryArgs,
16
+ ): Promise<void> {
17
+ const { entry_id, note_timestamp, sheet_name } = args
18
+ const db = await read_db()
19
+ const sheet = get_sheet(db, sheet_name)
20
+ const entry = sheet.entries.find(({ id }) => id === entry_id)
21
+
22
+ if (entry === undefined) {
23
+ throw new Error(`Entry ${entry_id} not found in sheet ${sheet_name}`)
24
+ }
25
+
26
+ const target_ms = new Date(note_timestamp).getTime()
27
+
28
+ if (Number.isNaN(target_ms)) {
29
+ throw new Error('Invalid note timestamp')
30
+ }
31
+
32
+ const note_index = entry.notes.findIndex(
33
+ ({ timestamp }) => timestamp.getTime() === target_ms,
34
+ )
35
+
36
+ if (note_index === -1) {
37
+ throw new Error('Note not found on entry')
38
+ }
39
+
40
+ entry.notes.splice(note_index, 1)
41
+ await write_db(db)
42
+ }
@@ -0,0 +1,30 @@
1
+ import { read_db } from "@/lib/read_db";
2
+ import { write_db } from "@/lib/write_db";
3
+
4
+ /**
5
+ * Removes a sheet and clears the active sheet when it was selected.
6
+ */
7
+ export async function delete_sheet(sheet_name: string): Promise<void> {
8
+ const db = await read_db();
9
+ const sheet_index = db.sheets.findIndex(({ name }) => name === sheet_name);
10
+
11
+ if (sheet_index === -1) {
12
+ throw new Error(`Sheet ${sheet_name} not found`);
13
+ }
14
+
15
+ if (db.activeSheetName === sheet_name) {
16
+ db.activeSheetName = null;
17
+ }
18
+
19
+ db.sheets.splice(sheet_index, 1);
20
+
21
+ if (db.sheets.length === 0) {
22
+ throw new Error("Cannot delete the last sheet");
23
+ }
24
+
25
+ if (db.activeSheetName === null) {
26
+ db.activeSheetName = db.sheets[0]?.name ?? null;
27
+ }
28
+
29
+ await write_db(db);
30
+ }
@@ -0,0 +1,22 @@
1
+ import { type TrackerState } from '@/lib/types/tracker_state'
2
+
3
+ /**
4
+ * Sends a DELETE request to a tracker API route and returns updated state.
5
+ */
6
+ export async function delete_tracker_action(
7
+ path: string,
8
+ body: unknown,
9
+ ): Promise<TrackerState> {
10
+ const response = await fetch(path, {
11
+ method: 'DELETE',
12
+ headers: { 'Content-Type': 'application/json' },
13
+ body: JSON.stringify(body),
14
+ })
15
+
16
+ if (!response.ok) {
17
+ const payload = (await response.json()) as { error?: string }
18
+ throw new Error(payload.error ?? 'Request failed')
19
+ }
20
+
21
+ return (await response.json()) as TrackerState
22
+ }