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,28 @@
1
+ import { SHEET_TAG_FILTERS_STORAGE_KEY } from '@/lib/types/ui_settings'
2
+
3
+ /**
4
+ * Reads per-sheet tag filter selections from localStorage.
5
+ */
6
+ export function read_stored_sheet_tag_filters(): Record<string, string[]> {
7
+ if (typeof window === 'undefined') {
8
+ return {}
9
+ }
10
+
11
+ try {
12
+ const raw = window.localStorage.getItem(SHEET_TAG_FILTERS_STORAGE_KEY)
13
+
14
+ if (raw === null) {
15
+ return {}
16
+ }
17
+
18
+ const parsed = JSON.parse(raw) as Record<string, string[]>
19
+
20
+ if (parsed === null || typeof parsed !== 'object') {
21
+ return {}
22
+ }
23
+
24
+ return parsed
25
+ } catch {
26
+ return {}
27
+ }
28
+ }
@@ -0,0 +1,18 @@
1
+ import { THEME_STORAGE_KEY, type Theme } from "@/lib/types/theme";
2
+
3
+ /**
4
+ * Reads the persisted theme from localStorage, if present.
5
+ */
6
+ export function read_stored_theme(): Theme | null {
7
+ if (typeof window === "undefined") {
8
+ return null;
9
+ }
10
+
11
+ const stored = window.localStorage.getItem(THEME_STORAGE_KEY);
12
+
13
+ if (stored === "light" || stored === "dark") {
14
+ return stored;
15
+ }
16
+
17
+ return null;
18
+ }
@@ -0,0 +1,39 @@
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 RenameSheetArgs {
6
+ sheet_name: string;
7
+ new_name: string;
8
+ }
9
+
10
+ /**
11
+ * Renames a sheet and updates the active sheet pointer when needed.
12
+ */
13
+ export async function rename_sheet(args: RenameSheetArgs): Promise<void> {
14
+ const { sheet_name } = args;
15
+ const trimmed = args.new_name.trim();
16
+
17
+ if (trimmed.length === 0) {
18
+ throw new Error("Sheet name must not be empty");
19
+ }
20
+
21
+ if (trimmed === sheet_name) {
22
+ return;
23
+ }
24
+
25
+ const db = await read_db();
26
+ const sheet = get_sheet(db, sheet_name);
27
+
28
+ if (db.sheets.some(({ name }) => name === trimmed)) {
29
+ throw new Error(`Sheet ${trimmed} already exists`);
30
+ }
31
+
32
+ sheet.name = trimmed;
33
+
34
+ if (db.activeSheetName === sheet_name) {
35
+ db.activeSheetName = trimmed;
36
+ }
37
+
38
+ await write_db(db);
39
+ }
@@ -0,0 +1,19 @@
1
+ import {
2
+ merge_tags_across_db,
3
+ type MergeTagsAcrossDbResult,
4
+ } from '@/lib/merge_tags_across_db'
5
+ import { tags_are_equal } from '@/lib/tags_are_equal'
6
+
7
+ /**
8
+ * Renames a tag on every entry that uses it.
9
+ */
10
+ export async function rename_tag_across_db(
11
+ from_tag: string,
12
+ to_tag: string,
13
+ ): Promise<MergeTagsAcrossDbResult> {
14
+ if (tags_are_equal(from_tag, to_tag)) {
15
+ throw new Error('Choose a different name for the tag.')
16
+ }
17
+
18
+ return merge_tags_across_db([from_tag], to_tag)
19
+ }
@@ -0,0 +1,36 @@
1
+ import { DEFAULT_SHEET_NAME } from '@/lib/config'
2
+ import { type TimeTrackerDB } from '@/lib/types'
3
+
4
+ /**
5
+ * Chooses which sheet to show: session preference, stored active sheet, then a running entry.
6
+ */
7
+ export function resolve_active_sheet_name(
8
+ db: TimeTrackerDB,
9
+ preferred_sheet_name?: string | null,
10
+ ): string {
11
+ const trimmed_preference = preferred_sheet_name?.trim() ?? ''
12
+
13
+ if (
14
+ trimmed_preference.length > 0 &&
15
+ db.sheets.some((sheet) => sheet.name === trimmed_preference)
16
+ ) {
17
+ return trimmed_preference
18
+ }
19
+
20
+ if (
21
+ db.activeSheetName !== null &&
22
+ db.sheets.some((sheet) => sheet.name === db.activeSheetName)
23
+ ) {
24
+ return db.activeSheetName
25
+ }
26
+
27
+ const sheet_with_active_entry = db.sheets.find(
28
+ (sheet) => sheet.activeEntryID !== null,
29
+ )
30
+
31
+ if (sheet_with_active_entry !== undefined) {
32
+ return sheet_with_active_entry.name
33
+ }
34
+
35
+ return db.sheets[0]?.name ?? DEFAULT_SHEET_NAME
36
+ }
@@ -0,0 +1,37 @@
1
+ import { type DefaultSheetSessionMode } from '@/lib/types/ui_settings'
2
+ import { type TimeTrackerDB } from '@/lib/types'
3
+
4
+ /**
5
+ * Resolves which sheet name to prefer on a new session before DB fallbacks.
6
+ */
7
+ export function resolve_session_preferred_sheet(
8
+ db: TimeTrackerDB,
9
+ mode: DefaultSheetSessionMode,
10
+ last_viewed_sheet: string | null,
11
+ fixed_sheet_name: string | null,
12
+ ): string | null {
13
+ switch (mode) {
14
+ case 'fixed': {
15
+ const trimmed = fixed_sheet_name?.trim() ?? ''
16
+
17
+ if (
18
+ trimmed.length > 0 &&
19
+ db.sheets.some((sheet) => sheet.name === trimmed)
20
+ ) {
21
+ return trimmed
22
+ }
23
+
24
+ return null
25
+ }
26
+ case 'active_timer': {
27
+ const sheet_with_timer = db.sheets.find(
28
+ (sheet) => sheet.activeEntryID !== null,
29
+ )
30
+
31
+ return sheet_with_timer?.name ?? null
32
+ }
33
+ case 'last_viewed':
34
+ default:
35
+ return last_viewed_sheet
36
+ }
37
+ }
@@ -0,0 +1,18 @@
1
+ import { type Theme } from "@/lib/types/theme";
2
+
3
+ /**
4
+ * Resolves the initial theme from storage or system preference.
5
+ */
6
+ export function resolve_theme(stored: Theme | null): Theme {
7
+ if (stored !== null) {
8
+ return stored;
9
+ }
10
+
11
+ if (typeof window === "undefined") {
12
+ return "dark";
13
+ }
14
+
15
+ return window.matchMedia("(prefers-color-scheme: light)").matches
16
+ ? "light"
17
+ : "dark";
18
+ }
@@ -0,0 +1,19 @@
1
+ import { type ThemeMode } from '@/lib/types/ui_preferences'
2
+ import { type Theme } from '@/lib/types/theme'
3
+
4
+ /**
5
+ * Resolves a theme mode preference to the applied light/dark theme.
6
+ */
7
+ export function resolve_theme_mode_to_theme(mode: ThemeMode): Theme {
8
+ if (mode === 'light' || mode === 'dark') {
9
+ return mode
10
+ }
11
+
12
+ if (typeof window === 'undefined') {
13
+ return 'dark'
14
+ }
15
+
16
+ return window.matchMedia('(prefers-color-scheme: light)').matches
17
+ ? 'light'
18
+ : 'dark'
19
+ }
@@ -0,0 +1,24 @@
1
+ import { convert_json_db } from '@/lib/convert_json_db'
2
+ import { is_json_time_tracker_db } from '@/lib/is_json_time_tracker_db'
3
+ import { migrate_json_db } from '@/lib/migrate_json_db'
4
+ import { write_db } from '@/lib/write_db'
5
+
6
+ /**
7
+ * Replaces the on-disk database with an uploaded backup after validation.
8
+ */
9
+ export async function restore_db_from_uploaded_json(
10
+ uploaded: unknown,
11
+ ): Promise<void> {
12
+ if (!is_json_time_tracker_db(uploaded)) {
13
+ throw new Error('Invalid backup file: expected a time tracker database.')
14
+ }
15
+
16
+ const { json_db } = migrate_json_db(uploaded)
17
+ const db = convert_json_db(json_db)
18
+
19
+ if (db.sheets.length === 0) {
20
+ throw new Error('Invalid backup file: the database must include at least one sheet.')
21
+ }
22
+
23
+ await write_db(db)
24
+ }
@@ -0,0 +1,27 @@
1
+ import { get_entry_duration_ms } from "@/lib/get_entry_duration_ms";
2
+ import { type SerializedEntry } from "@/lib/types/tracker_state";
3
+ import { type TimeSheetEntry } from "@/lib/types";
4
+
5
+ /**
6
+ * Converts an in-memory entry into a JSON-safe tracker payload.
7
+ */
8
+ export function serialize_entry(
9
+ entry: TimeSheetEntry,
10
+ sheet_name: string,
11
+ is_active: boolean,
12
+ ): SerializedEntry {
13
+ return {
14
+ id: entry.id,
15
+ description: entry.description,
16
+ start: entry.start.toISOString(),
17
+ end: entry.end === null ? null : entry.end.toISOString(),
18
+ tags: entry.tags,
19
+ notes: entry.notes.map(({ timestamp, text }) => ({
20
+ timestamp: timestamp.toISOString(),
21
+ text,
22
+ })),
23
+ sheetName: sheet_name,
24
+ durationMs: get_entry_duration_ms(entry),
25
+ isActive: is_active,
26
+ };
27
+ }
@@ -0,0 +1,19 @@
1
+ import { type ReportingSourceSheet } from '@/lib/types/reporting'
2
+ import { type TimeSheet } from '@/lib/types'
3
+
4
+ /**
5
+ * Serializes time sheets for client-side reporting calculations.
6
+ */
7
+ export function serialize_reporting_source_sheets(
8
+ sheets: TimeSheet[],
9
+ ): ReportingSourceSheet[] {
10
+ return sheets.map((sheet) => ({
11
+ name: sheet.name,
12
+ activeEntryID: sheet.activeEntryID,
13
+ entries: sheet.entries.map((entry) => ({
14
+ id: entry.id,
15
+ start: entry.start.toISOString(),
16
+ end: entry.end === null ? null : entry.end.toISOString(),
17
+ })),
18
+ }))
19
+ }
@@ -0,0 +1,18 @@
1
+ import { serialize_entry } from '@/lib/serialize_entry'
2
+ import { type SerializedEntry } from '@/lib/types/tracker_state'
3
+ import { type TimeSheet } from '@/lib/types'
4
+
5
+ /**
6
+ * Serializes every entry on a time sheet for the web UI.
7
+ */
8
+ export function serialize_sheet_entries(sheet: TimeSheet): SerializedEntry[] {
9
+ const { activeEntryID, entries, name } = sheet
10
+
11
+ return entries.map((entry) =>
12
+ serialize_entry(
13
+ entry,
14
+ name,
15
+ activeEntryID === entry.id && entry.end === null,
16
+ ),
17
+ )
18
+ }
@@ -0,0 +1,12 @@
1
+ import { accent_color_preference } from '@/lib/preferences/accent_color_preference'
2
+ import { apply_accent_color } from '@/lib/apply_accent_color'
3
+ import { type AccentColor } from '@/lib/types/ui_preferences'
4
+
5
+ /**
6
+ * Persists the accent color preference, applies it, and notifies subscribers.
7
+ */
8
+ export function set_accent_color(value: AccentColor): void {
9
+ accent_color_preference.write(value)
10
+ apply_accent_color(value)
11
+ accent_color_preference.notify()
12
+ }
@@ -0,0 +1,18 @@
1
+ import { gen_sheet } from "@/lib/gen_db";
2
+ import { read_db } from "@/lib/read_db";
3
+ import { write_db } from "@/lib/write_db";
4
+
5
+ /**
6
+ * Switches the active sheet, creating it when missing.
7
+ */
8
+ export async function set_active_sheet(sheet_name: string): Promise<void> {
9
+ const db = await read_db();
10
+ const exists = db.sheets.some(({ name }) => name === sheet_name);
11
+
12
+ if (!exists) {
13
+ db.sheets.push(gen_sheet(sheet_name));
14
+ }
15
+
16
+ db.activeSheetName = sheet_name;
17
+ await write_db(db);
18
+ }
@@ -0,0 +1,12 @@
1
+ import { apply_color_palette } from '@/lib/apply_color_palette'
2
+ import { color_palette_preference } from '@/lib/preferences/color_palette_preference'
3
+ import { type ColorPalette } from '@/lib/types/ui_preferences'
4
+
5
+ /**
6
+ * Persists the color palette preference, applies it, and notifies subscribers.
7
+ */
8
+ export function set_color_palette(value: ColorPalette): void {
9
+ color_palette_preference.write(value)
10
+ apply_color_palette(value)
11
+ color_palette_preference.notify()
12
+ }
@@ -0,0 +1,12 @@
1
+ import { apply_compact_lists } from "@/lib/apply_compact_lists";
2
+ import { notify_compact_lists_subscribers } from "@/lib/subscribe_compact_lists";
3
+ import { write_stored_compact_lists } from "@/lib/write_stored_compact_lists";
4
+
5
+ /**
6
+ * Updates the compact lists UI setting in the DOM and localStorage.
7
+ */
8
+ export function set_compact_lists(enabled: boolean): void {
9
+ apply_compact_lists(enabled);
10
+ write_stored_compact_lists(enabled);
11
+ notify_compact_lists_subscribers();
12
+ }
@@ -0,0 +1,8 @@
1
+ import { write_stored_default_sheet_fixed_name } from '@/lib/write_stored_default_sheet_fixed_name'
2
+
3
+ /**
4
+ * Updates which sheet opens when the session mode is set to a specific sheet.
5
+ */
6
+ export function set_default_sheet_fixed_name(sheet_name: string): void {
7
+ write_stored_default_sheet_fixed_name(sheet_name)
8
+ }
@@ -0,0 +1,11 @@
1
+ import { write_stored_default_sheet_session_mode } from '@/lib/write_stored_default_sheet_session_mode'
2
+ import { type DefaultSheetSessionMode } from '@/lib/types/ui_settings'
3
+
4
+ /**
5
+ * Updates the default sheet session mode preference.
6
+ */
7
+ export function set_default_sheet_session_mode(
8
+ mode: DefaultSheetSessionMode,
9
+ ): void {
10
+ write_stored_default_sheet_session_mode(mode)
11
+ }
@@ -0,0 +1,13 @@
1
+ import { notify_sheet_tag_filters_subscribers } from '@/lib/subscribe_sheet_tag_filters'
2
+ import { write_sheet_tag_filter } from '@/lib/write_sheet_tag_filter'
3
+
4
+ /**
5
+ * Replaces the tag filter list for a sheet.
6
+ */
7
+ export function set_sheet_tag_filter(
8
+ sheet_name: string,
9
+ filter_tags: string[],
10
+ ): void {
11
+ write_sheet_tag_filter(sheet_name, filter_tags)
12
+ notify_sheet_tag_filters_subscribers()
13
+ }
@@ -0,0 +1,19 @@
1
+ import { apply_theme } from '@/lib/apply_theme'
2
+ import { notify_theme_subscribers } from '@/lib/subscribe_theme'
3
+ import { resolve_theme_mode_to_theme } from '@/lib/resolve_theme_mode_to_theme'
4
+ import { theme_mode_preference } from '@/lib/preferences/theme_mode_preference'
5
+ import { write_stored_theme } from '@/lib/write_stored_theme'
6
+ import { type ThemeMode } from '@/lib/types/ui_preferences'
7
+
8
+ /**
9
+ * Persists the theme mode, applies the resolved theme, and notifies subscribers.
10
+ */
11
+ export function set_theme_mode(mode: ThemeMode): void {
12
+ theme_mode_preference.write(mode)
13
+ theme_mode_preference.notify()
14
+
15
+ const resolved = resolve_theme_mode_to_theme(mode)
16
+ apply_theme(resolved)
17
+ write_stored_theme(resolved)
18
+ notify_theme_subscribers()
19
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Stable empty tag filter list for useSyncExternalStore snapshots.
3
+ */
4
+ export const EMPTY_SHEET_TAG_FILTER: readonly string[] = []
5
+
6
+ interface SheetTagFilterSnapshotCache {
7
+ sheet_name: string
8
+ tags_key: string
9
+ snapshot: readonly string[]
10
+ }
11
+
12
+ let snapshot_cache: SheetTagFilterSnapshotCache | null = null
13
+
14
+ /**
15
+ * Clears the cached sheet tag filter snapshot.
16
+ */
17
+ export function reset_sheet_tag_filter_snapshot_cache(): void {
18
+ snapshot_cache = null
19
+ }
20
+
21
+ /**
22
+ * Returns a stable snapshot reference for the given tag list.
23
+ */
24
+ export function get_stable_sheet_tag_filter_snapshot(
25
+ sheet_name: string,
26
+ tags: string[],
27
+ ): readonly string[] {
28
+ const tags_key = tags.join('\0')
29
+
30
+ if (
31
+ snapshot_cache !== null &&
32
+ snapshot_cache.sheet_name === sheet_name &&
33
+ snapshot_cache.tags_key === tags_key
34
+ ) {
35
+ return snapshot_cache.snapshot
36
+ }
37
+
38
+ const snapshot =
39
+ tags.length === 0 ? EMPTY_SHEET_TAG_FILTER : Object.freeze([...tags])
40
+
41
+ snapshot_cache = {
42
+ sheet_name,
43
+ tags_key,
44
+ snapshot,
45
+ }
46
+
47
+ return snapshot
48
+ }
@@ -0,0 +1,35 @@
1
+ import { type EntryListSort } from '@/lib/types/ui_preferences'
2
+ import { type SerializedEntry } from '@/lib/types/tracker_state'
3
+
4
+ /**
5
+ * Sorts serialized entries for display in the entry list.
6
+ */
7
+ export function sort_serialized_entries(
8
+ entries: SerializedEntry[],
9
+ sort: EntryListSort,
10
+ ): SerializedEntry[] {
11
+ const sorted = [...entries]
12
+
13
+ switch (sort) {
14
+ case 'oldest':
15
+ return sorted.sort(
16
+ (left, right) =>
17
+ new Date(left.start).getTime() - new Date(right.start).getTime(),
18
+ )
19
+ case 'duration':
20
+ return sorted.sort((left, right) => right.durationMs - left.durationMs)
21
+ case 'description':
22
+ return sorted.sort((left, right) => {
23
+ const left_label = left.description.trim() || 'Untitled entry'
24
+ const right_label = right.description.trim() || 'Untitled entry'
25
+
26
+ return left_label.localeCompare(right_label)
27
+ })
28
+ case 'newest':
29
+ default:
30
+ return sorted.sort(
31
+ (left, right) =>
32
+ new Date(right.start).getTime() - new Date(left.start).getTime(),
33
+ )
34
+ }
35
+ }
@@ -0,0 +1,43 @@
1
+ import { type SheetReportSort, type SheetReportStats } from '@/lib/types/reporting'
2
+
3
+ /**
4
+ * Returns a new array of sheet report stats in the requested order.
5
+ */
6
+ export function sort_sheet_report_stats(
7
+ sheets: SheetReportStats[],
8
+ sort: SheetReportSort,
9
+ ): SheetReportStats[] {
10
+ const sorted = [...sheets]
11
+
12
+ switch (sort) {
13
+ case 'name':
14
+ sorted.sort((left, right) =>
15
+ left.sheetName.localeCompare(right.sheetName),
16
+ )
17
+ break
18
+ case 'entry_count':
19
+ sorted.sort((left, right) => {
20
+ if (right.entryCount !== left.entryCount) {
21
+ return right.entryCount - left.entryCount
22
+ }
23
+
24
+ return right.totalMs - left.totalMs
25
+ })
26
+ break
27
+ case 'active_first':
28
+ sorted.sort((left, right) => {
29
+ if (left.hasActiveEntry !== right.hasActiveEntry) {
30
+ return left.hasActiveEntry ? -1 : 1
31
+ }
32
+
33
+ return right.totalMs - left.totalMs
34
+ })
35
+ break
36
+ case 'duration':
37
+ default:
38
+ sorted.sort((left, right) => right.totalMs - left.totalMs)
39
+ break
40
+ }
41
+
42
+ return sorted
43
+ }
@@ -0,0 +1,25 @@
1
+ type CompactListsListener = () => void;
2
+
3
+ const compact_lists_listeners = new Set<CompactListsListener>();
4
+
5
+ /**
6
+ * Subscribes to compact lists setting changes for useSyncExternalStore.
7
+ */
8
+ export function subscribe_compact_lists(
9
+ listener: CompactListsListener,
10
+ ): () => void {
11
+ compact_lists_listeners.add(listener);
12
+
13
+ return () => {
14
+ compact_lists_listeners.delete(listener);
15
+ };
16
+ }
17
+
18
+ /**
19
+ * Notifies subscribers after the compact lists setting changes.
20
+ */
21
+ export function notify_compact_lists_subscribers(): void {
22
+ compact_lists_listeners.forEach((listener) => {
23
+ listener();
24
+ });
25
+ }
@@ -0,0 +1,28 @@
1
+ import { reset_sheet_tag_filter_snapshot_cache } from '@/lib/sheet_tag_filter_snapshots'
2
+
3
+ type SheetTagFiltersListener = () => void
4
+
5
+ const sheet_tag_filters_listeners = new Set<SheetTagFiltersListener>()
6
+
7
+ /**
8
+ * Subscribes to sheet tag filter changes for useSyncExternalStore.
9
+ */
10
+ export function subscribe_sheet_tag_filters(
11
+ listener: SheetTagFiltersListener,
12
+ ): () => void {
13
+ sheet_tag_filters_listeners.add(listener)
14
+
15
+ return () => {
16
+ sheet_tag_filters_listeners.delete(listener)
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Notifies subscribers after sheet tag filters change.
22
+ */
23
+ export function notify_sheet_tag_filters_subscribers(): void {
24
+ reset_sheet_tag_filter_snapshot_cache()
25
+ sheet_tag_filters_listeners.forEach((listener) => {
26
+ listener()
27
+ })
28
+ }
@@ -0,0 +1,23 @@
1
+ type ThemeListener = () => void;
2
+
3
+ const theme_listeners = new Set<ThemeListener>();
4
+
5
+ /**
6
+ * Subscribes to theme changes for useSyncExternalStore.
7
+ */
8
+ export function subscribe_theme(listener: ThemeListener): () => void {
9
+ theme_listeners.add(listener);
10
+
11
+ return () => {
12
+ theme_listeners.delete(listener);
13
+ };
14
+ }
15
+
16
+ /**
17
+ * Notifies all theme subscribers after the DOM theme changes.
18
+ */
19
+ export function notify_theme_subscribers(): void {
20
+ theme_listeners.forEach((listener) => {
21
+ listener();
22
+ });
23
+ }
@@ -0,0 +1,19 @@
1
+ import { write_active_sheet_preference } from '@/lib/write_active_sheet_preference'
2
+ import { type TrackerState } from '@/lib/types/tracker_state'
3
+
4
+ /**
5
+ * Updates stored sheet preference from the current tracker state.
6
+ */
7
+ export function sync_active_sheet_preference(state: TrackerState): void {
8
+ if (typeof window === 'undefined') {
9
+ return
10
+ }
11
+
12
+ const sheet_name = state.activeSheetName
13
+
14
+ if (sheet_name === null) {
15
+ return
16
+ }
17
+
18
+ write_active_sheet_preference(sheet_name)
19
+ }
@@ -0,0 +1,12 @@
1
+ import { normalize_stored_tag } from '@/lib/normalize_stored_tag'
2
+
3
+ /**
4
+ * Returns whether two stored tags refer to the same tag name.
5
+ */
6
+ export function tags_are_equal(left: string, right: string): boolean {
7
+ try {
8
+ return normalize_stored_tag(left) === normalize_stored_tag(right)
9
+ } catch {
10
+ return false
11
+ }
12
+ }