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,18 @@
1
+ import { notify_settings_saved } from '@/lib/notify_settings_saved'
2
+ import { type UiPreferenceStore } from '@/lib/ui_preference_store'
3
+
4
+ /**
5
+ * Writes a UI preference, notifies subscribers, and shows the saved toast.
6
+ */
7
+ export function persist_ui_preference<T extends string>(
8
+ store: UiPreferenceStore<T>,
9
+ value: T,
10
+ ): void {
11
+ if (store.read() === value) {
12
+ return
13
+ }
14
+
15
+ store.write(value)
16
+ store.notify()
17
+ notify_settings_saved()
18
+ }
@@ -0,0 +1,22 @@
1
+ import { type TrackerState } from "@/lib/types/tracker_state";
2
+
3
+ /**
4
+ * Posts a JSON body to a tracker API route and returns updated state.
5
+ */
6
+ export async function post_tracker_action(
7
+ path: string,
8
+ body: unknown,
9
+ ): Promise<TrackerState> {
10
+ const response = await fetch(path, {
11
+ method: "POST",
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
+ }
@@ -0,0 +1,21 @@
1
+ import { create_ui_preference_store } from '@/lib/ui_preference_store'
2
+ import {
3
+ ACCENT_COLOR_DEFAULT,
4
+ ACCENT_COLOR_STORAGE_KEY,
5
+ ACCENT_COLOR_VALUES,
6
+ type AccentColor,
7
+ } from '@/lib/types/ui_preferences'
8
+
9
+ const valid_set = new Set<string>(ACCENT_COLOR_VALUES)
10
+
11
+ const is_accent_color = (value: string): value is AccentColor =>
12
+ valid_set.has(value)
13
+
14
+ /**
15
+ * Accent color preset preference store.
16
+ */
17
+ export const accent_color_preference = create_ui_preference_store<AccentColor>({
18
+ storage_key: ACCENT_COLOR_STORAGE_KEY,
19
+ default_value: ACCENT_COLOR_DEFAULT,
20
+ is_valid: is_accent_color,
21
+ })
@@ -0,0 +1,20 @@
1
+ import { create_ui_preference_store } from '@/lib/ui_preference_store'
2
+ import {
3
+ CHECK_IN_FORM_COLLAPSED_DEFAULT,
4
+ CHECK_IN_FORM_COLLAPSED_STORAGE_KEY,
5
+ type CheckInFormCollapsed,
6
+ } from '@/lib/types/ui_preferences'
7
+
8
+ const is_check_in_form_collapsed = (
9
+ value: string,
10
+ ): value is CheckInFormCollapsed => value === 'true' || value === 'false'
11
+
12
+ /**
13
+ * Whether the check-in form should be collapsed by default.
14
+ */
15
+ export const check_in_form_collapsed_preference =
16
+ create_ui_preference_store<CheckInFormCollapsed>({
17
+ storage_key: CHECK_IN_FORM_COLLAPSED_STORAGE_KEY,
18
+ default_value: CHECK_IN_FORM_COLLAPSED_DEFAULT,
19
+ is_valid: is_check_in_form_collapsed,
20
+ })
@@ -0,0 +1,20 @@
1
+ import { create_ui_preference_store } from '@/lib/ui_preference_store'
2
+ import {
3
+ CLEAR_TAG_FILTERS_ON_SHEET_CHANGE_DEFAULT,
4
+ CLEAR_TAG_FILTERS_ON_SHEET_CHANGE_STORAGE_KEY,
5
+ type ClearTagFiltersOnSheetChange,
6
+ } from '@/lib/types/ui_preferences'
7
+
8
+ const is_clear_tag_filters_on_sheet_change = (
9
+ value: string,
10
+ ): value is ClearTagFiltersOnSheetChange => value === 'true' || value === 'false'
11
+
12
+ /**
13
+ * Whether tag filters reset whenever the active sheet changes.
14
+ */
15
+ export const clear_tag_filters_on_sheet_change_preference =
16
+ create_ui_preference_store<ClearTagFiltersOnSheetChange>({
17
+ storage_key: CLEAR_TAG_FILTERS_ON_SHEET_CHANGE_STORAGE_KEY,
18
+ default_value: CLEAR_TAG_FILTERS_ON_SHEET_CHANGE_DEFAULT,
19
+ is_valid: is_clear_tag_filters_on_sheet_change,
20
+ })
@@ -0,0 +1,21 @@
1
+ import { create_ui_preference_store } from '@/lib/ui_preference_store'
2
+ import {
3
+ COLOR_PALETTE_DEFAULT,
4
+ COLOR_PALETTE_STORAGE_KEY,
5
+ COLOR_PALETTE_VALUES,
6
+ type ColorPalette,
7
+ } from '@/lib/types/ui_preferences'
8
+
9
+ const valid_set = new Set<string>(COLOR_PALETTE_VALUES)
10
+
11
+ const is_color_palette = (value: string): value is ColorPalette =>
12
+ valid_set.has(value)
13
+
14
+ /**
15
+ * Base color palette preference store (surfaces and backgrounds).
16
+ */
17
+ export const color_palette_preference = create_ui_preference_store<ColorPalette>({
18
+ storage_key: COLOR_PALETTE_STORAGE_KEY,
19
+ default_value: COLOR_PALETTE_DEFAULT,
20
+ is_valid: is_color_palette,
21
+ })
@@ -0,0 +1,20 @@
1
+ import { create_ui_preference_store } from '@/lib/ui_preference_store'
2
+ import {
3
+ CONFIRM_BEFORE_CHECKOUT_DEFAULT,
4
+ CONFIRM_BEFORE_CHECKOUT_STORAGE_KEY,
5
+ type ConfirmBeforeCheckout,
6
+ } from '@/lib/types/ui_preferences'
7
+
8
+ const is_confirm_before_checkout = (
9
+ value: string,
10
+ ): value is ConfirmBeforeCheckout => value === 'true' || value === 'false'
11
+
12
+ /**
13
+ * Whether to confirm before checking out of an active timer.
14
+ */
15
+ export const confirm_before_checkout_preference =
16
+ create_ui_preference_store<ConfirmBeforeCheckout>({
17
+ storage_key: CONFIRM_BEFORE_CHECKOUT_STORAGE_KEY,
18
+ default_value: CONFIRM_BEFORE_CHECKOUT_DEFAULT,
19
+ is_valid: is_confirm_before_checkout,
20
+ })
@@ -0,0 +1,20 @@
1
+ import { create_ui_preference_store } from '@/lib/ui_preference_store'
2
+ import {
3
+ CONFIRM_DESTRUCTIVE_ACTIONS_DEFAULT,
4
+ CONFIRM_DESTRUCTIVE_ACTIONS_STORAGE_KEY,
5
+ type ConfirmDestructiveActions,
6
+ } from '@/lib/types/ui_preferences'
7
+
8
+ const is_confirm_destructive_actions = (
9
+ value: string,
10
+ ): value is ConfirmDestructiveActions => value === 'true' || value === 'false'
11
+
12
+ /**
13
+ * Whether to show a confirm dialog before deleting entries or sheets.
14
+ */
15
+ export const confirm_destructive_actions_preference =
16
+ create_ui_preference_store<ConfirmDestructiveActions>({
17
+ storage_key: CONFIRM_DESTRUCTIVE_ACTIONS_STORAGE_KEY,
18
+ default_value: CONFIRM_DESTRUCTIVE_ACTIONS_DEFAULT,
19
+ is_valid: is_confirm_destructive_actions,
20
+ })
@@ -0,0 +1,21 @@
1
+ import { create_ui_preference_store } from '@/lib/ui_preference_store'
2
+ import {
3
+ DEFAULT_REPORTING_RANGE_DEFAULT,
4
+ DEFAULT_REPORTING_RANGE_STORAGE_KEY,
5
+ type DefaultReportingRange,
6
+ } from '@/lib/types/ui_preferences'
7
+
8
+ const is_default_reporting_range = (
9
+ value: string,
10
+ ): value is DefaultReportingRange =>
11
+ value === 'none' || value === 'today' || value === 'week'
12
+
13
+ /**
14
+ * Initial date range when opening the reporting view.
15
+ */
16
+ export const default_reporting_range_preference =
17
+ create_ui_preference_store<DefaultReportingRange>({
18
+ storage_key: DEFAULT_REPORTING_RANGE_STORAGE_KEY,
19
+ default_value: DEFAULT_REPORTING_RANGE_DEFAULT,
20
+ is_valid: is_default_reporting_range,
21
+ })
@@ -0,0 +1,24 @@
1
+ import { create_ui_preference_store } from '@/lib/ui_preference_store'
2
+ import {
3
+ DEFAULT_REPORTING_SORT_DEFAULT,
4
+ DEFAULT_REPORTING_SORT_STORAGE_KEY,
5
+ type DefaultReportingSort,
6
+ } from '@/lib/types/ui_preferences'
7
+
8
+ const is_default_reporting_sort = (
9
+ value: string,
10
+ ): value is DefaultReportingSort =>
11
+ value === 'duration' ||
12
+ value === 'name' ||
13
+ value === 'entry_count' ||
14
+ value === 'active_first'
15
+
16
+ /**
17
+ * Default reporting sort preference store.
18
+ */
19
+ export const default_reporting_sort_preference =
20
+ create_ui_preference_store<DefaultReportingSort>({
21
+ storage_key: DEFAULT_REPORTING_SORT_STORAGE_KEY,
22
+ default_value: DEFAULT_REPORTING_SORT_DEFAULT,
23
+ is_valid: is_default_reporting_sort,
24
+ })
@@ -0,0 +1,19 @@
1
+ import { create_ui_preference_store } from '@/lib/ui_preference_store'
2
+ import {
3
+ DURATION_FORMAT_DEFAULT,
4
+ DURATION_FORMAT_STORAGE_KEY,
5
+ type DurationFormat,
6
+ } from '@/lib/types/ui_preferences'
7
+
8
+ const is_duration_format = (value: string): value is DurationFormat =>
9
+ value === 'humanized' || value === 'clock' || value === 'decimal'
10
+
11
+ /**
12
+ * Duration format preference store: humanized, clock, or decimal.
13
+ */
14
+ export const duration_format_preference =
15
+ create_ui_preference_store<DurationFormat>({
16
+ storage_key: DURATION_FORMAT_STORAGE_KEY,
17
+ default_value: DURATION_FORMAT_DEFAULT,
18
+ is_valid: is_duration_format,
19
+ })
@@ -0,0 +1,21 @@
1
+ import { create_ui_preference_store } from '@/lib/ui_preference_store'
2
+ import {
3
+ ENTRY_LIST_SORT_DEFAULT,
4
+ ENTRY_LIST_SORT_STORAGE_KEY,
5
+ type EntryListSort,
6
+ } from '@/lib/types/ui_preferences'
7
+
8
+ const is_entry_list_sort = (value: string): value is EntryListSort =>
9
+ value === 'newest' ||
10
+ value === 'oldest' ||
11
+ value === 'duration' ||
12
+ value === 'description'
13
+
14
+ /**
15
+ * Default sort order for entry lists on the tracker home view.
16
+ */
17
+ export const entry_list_sort_preference = create_ui_preference_store<EntryListSort>({
18
+ storage_key: ENTRY_LIST_SORT_STORAGE_KEY,
19
+ default_value: ENTRY_LIST_SORT_DEFAULT,
20
+ is_valid: is_entry_list_sort,
21
+ })
@@ -0,0 +1,18 @@
1
+ import { create_ui_preference_store } from '@/lib/ui_preference_store'
2
+ import {
3
+ TAG_FILTER_MODE_DEFAULT,
4
+ TAG_FILTER_MODE_STORAGE_KEY,
5
+ type TagFilterMode,
6
+ } from '@/lib/types/ui_preferences'
7
+
8
+ const is_tag_filter_mode = (value: string): value is TagFilterMode =>
9
+ value === 'all' || value === 'any'
10
+
11
+ /**
12
+ * Tag filter match mode: require all selected tags or any one.
13
+ */
14
+ export const tag_filter_mode_preference = create_ui_preference_store<TagFilterMode>({
15
+ storage_key: TAG_FILTER_MODE_STORAGE_KEY,
16
+ default_value: TAG_FILTER_MODE_DEFAULT,
17
+ is_valid: is_tag_filter_mode,
18
+ })
@@ -0,0 +1,18 @@
1
+ import { create_ui_preference_store } from '@/lib/ui_preference_store'
2
+ import {
3
+ THEME_MODE_DEFAULT,
4
+ THEME_MODE_STORAGE_KEY,
5
+ type ThemeMode,
6
+ } from '@/lib/types/ui_preferences'
7
+
8
+ const is_theme_mode = (value: string): value is ThemeMode =>
9
+ value === 'light' || value === 'dark' || value === 'system'
10
+
11
+ /**
12
+ * Theme preference store: light, dark, or follow system.
13
+ */
14
+ export const theme_mode_preference = create_ui_preference_store<ThemeMode>({
15
+ storage_key: THEME_MODE_STORAGE_KEY,
16
+ default_value: THEME_MODE_DEFAULT,
17
+ is_valid: is_theme_mode,
18
+ })
@@ -0,0 +1,18 @@
1
+ import { create_ui_preference_store } from '@/lib/ui_preference_store'
2
+ import {
3
+ TIME_FORMAT_DEFAULT,
4
+ TIME_FORMAT_STORAGE_KEY,
5
+ type TimeFormat,
6
+ } from '@/lib/types/ui_preferences'
7
+
8
+ const is_time_format = (value: string): value is TimeFormat =>
9
+ value === '12h' || value === '24h'
10
+
11
+ /**
12
+ * Time format preference store: 12h or 24h.
13
+ */
14
+ export const time_format_preference = create_ui_preference_store<TimeFormat>({
15
+ storage_key: TIME_FORMAT_STORAGE_KEY,
16
+ default_value: TIME_FORMAT_DEFAULT,
17
+ is_valid: is_time_format,
18
+ })
@@ -0,0 +1,18 @@
1
+ import { create_ui_preference_store } from '@/lib/ui_preference_store'
2
+ import {
3
+ TIMER_IN_TITLE_DEFAULT,
4
+ TIMER_IN_TITLE_STORAGE_KEY,
5
+ type TimerInTitle,
6
+ } from '@/lib/types/ui_preferences'
7
+
8
+ const is_timer_in_title = (value: string): value is TimerInTitle =>
9
+ value === 'true' || value === 'false'
10
+
11
+ /**
12
+ * Whether the browser tab title shows the live timer while tracking.
13
+ */
14
+ export const timer_in_title_preference = create_ui_preference_store<TimerInTitle>({
15
+ storage_key: TIMER_IN_TITLE_STORAGE_KEY,
16
+ default_value: TIMER_IN_TITLE_DEFAULT,
17
+ is_valid: is_timer_in_title,
18
+ })
@@ -0,0 +1,19 @@
1
+ import { create_ui_preference_store } from '@/lib/ui_preference_store'
2
+ import {
3
+ TIMER_SHOW_SECONDS_DEFAULT,
4
+ TIMER_SHOW_SECONDS_STORAGE_KEY,
5
+ type TimerShowSeconds,
6
+ } from '@/lib/types/ui_preferences'
7
+
8
+ const is_timer_show_seconds = (value: string): value is TimerShowSeconds =>
9
+ value === 'true' || value === 'false'
10
+
11
+ /**
12
+ * Whether the active timer displays seconds.
13
+ */
14
+ export const timer_show_seconds_preference =
15
+ create_ui_preference_store<TimerShowSeconds>({
16
+ storage_key: TIMER_SHOW_SECONDS_STORAGE_KEY,
17
+ default_value: TIMER_SHOW_SECONDS_DEFAULT,
18
+ is_valid: is_timer_show_seconds,
19
+ })
@@ -0,0 +1,19 @@
1
+ import { create_ui_preference_store } from '@/lib/ui_preference_store'
2
+ import {
3
+ WEEK_STARTS_ON_DEFAULT,
4
+ WEEK_STARTS_ON_STORAGE_KEY,
5
+ type WeekStartsOn,
6
+ } from '@/lib/types/ui_preferences'
7
+
8
+ const is_week_starts_on = (value: string): value is WeekStartsOn =>
9
+ value === 'monday' || value === 'sunday'
10
+
11
+ /**
12
+ * Week start preference store: monday or sunday.
13
+ */
14
+ export const week_starts_on_preference =
15
+ create_ui_preference_store<WeekStartsOn>({
16
+ storage_key: WEEK_STARTS_ON_STORAGE_KEY,
17
+ default_value: WEEK_STARTS_ON_DEFAULT,
18
+ is_valid: is_week_starts_on,
19
+ })
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Prompts for a natural-language checkout time such as "30 minutes ago".
3
+ */
4
+ export function prompt_check_out_at(): string | null {
5
+ const value = window.prompt(
6
+ "Check out at what time?",
7
+ "30 minutes ago",
8
+ );
9
+
10
+ if (value === null) {
11
+ return null;
12
+ }
13
+
14
+ const trimmed = value.trim();
15
+
16
+ return trimmed.length === 0 ? null : trimmed;
17
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Prompts for note text to attach to a time sheet entry.
3
+ */
4
+ export function prompt_entry_note(): string | null {
5
+ const value = window.prompt("Add note", "");
6
+
7
+ if (value === null) {
8
+ return null;
9
+ }
10
+
11
+ const trimmed = value.trim();
12
+
13
+ return trimmed.length === 0 ? null : trimmed;
14
+ }
@@ -0,0 +1,27 @@
1
+ import { read_sheet_tag_filter } from '@/lib/read_sheet_tag_filter'
2
+ import { set_sheet_tag_filter } from '@/lib/set_sheet_tag_filter'
3
+ import { tags_are_equal } from '@/lib/tags_are_equal'
4
+
5
+ /**
6
+ * Drops filter tags that no longer appear on the sheet.
7
+ */
8
+ export function prune_sheet_tag_filter(
9
+ sheet_name: string,
10
+ available_tags: string[],
11
+ ): void {
12
+ const current = read_sheet_tag_filter(sheet_name)
13
+
14
+ if (current.length === 0) {
15
+ return
16
+ }
17
+
18
+ const pruned = current.filter((filter_tag) =>
19
+ available_tags.some((available_tag) =>
20
+ tags_are_equal(filter_tag, available_tag),
21
+ ),
22
+ )
23
+
24
+ if (pruned.length !== current.length) {
25
+ set_sheet_tag_filter(sheet_name, pruned)
26
+ }
27
+ }
package/lib/read_db.ts ADDED
@@ -0,0 +1,49 @@
1
+ import path from "node:path";
2
+ import { promises as fs } from "node:fs";
3
+
4
+ import { DB_PATH } from "@/lib/config";
5
+ import { convert_json_db } from "@/lib/convert_json_db";
6
+ import { ensure_dir_exists } from "@/lib/ensure_dir_exists";
7
+ import { gen_db } from "@/lib/gen_db";
8
+ import { migrate_json_db } from "@/lib/migrate_json_db";
9
+ import { write_db } from "@/lib/write_db";
10
+ import { type JSONTimeTrackerDB, type TimeTrackerDB } from "@/lib/types";
11
+
12
+ /**
13
+ * Loads the tracker database from disk, migrating when needed.
14
+ */
15
+ export async function read_db(
16
+ db_path: string = DB_PATH,
17
+ ): Promise<TimeTrackerDB> {
18
+ const db_path_dir = path.dirname(db_path);
19
+ await ensure_dir_exists(db_path_dir);
20
+
21
+ try {
22
+ await fs.access(db_path);
23
+ } catch {
24
+ const db = gen_db();
25
+ await write_db(db, db_path);
26
+ return db;
27
+ }
28
+
29
+ const db_json = await fs.readFile(db_path, "utf-8");
30
+ let json_db: JSONTimeTrackerDB;
31
+
32
+ try {
33
+ json_db = JSON.parse(db_json) as JSONTimeTrackerDB;
34
+ } catch (parse_err: unknown) {
35
+ throw new Error(`DB at ${db_path} is invalid JSON: ${String(parse_err)}`, {
36
+ cause: parse_err,
37
+ });
38
+ }
39
+
40
+ const migration_result = migrate_json_db(json_db);
41
+ json_db = migration_result.json_db;
42
+ const db = convert_json_db(json_db);
43
+
44
+ if (migration_result.did_migrate) {
45
+ await write_db(db, db_path);
46
+ }
47
+
48
+ return db;
49
+ }
@@ -0,0 +1,22 @@
1
+ import { promises as fs } from 'node:fs'
2
+
3
+ import { DB_PATH } from '@/lib/config'
4
+ import { read_db } from '@/lib/read_db'
5
+ import { write_db } from '@/lib/write_db'
6
+
7
+ /**
8
+ * Reads the on-disk database JSON used for backup downloads.
9
+ */
10
+ export async function read_db_backup_contents(
11
+ db_path: string = DB_PATH,
12
+ ): Promise<string> {
13
+ try {
14
+ return await fs.readFile(db_path, 'utf-8')
15
+ } catch {
16
+ const db = await read_db(db_path)
17
+
18
+ await write_db(db, db_path)
19
+
20
+ return await fs.readFile(db_path, 'utf-8')
21
+ }
22
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Reads whether compact lists are enabled on the document element.
3
+ */
4
+ export function read_document_compact_lists(): boolean {
5
+ if (typeof document === "undefined") {
6
+ return false;
7
+ }
8
+
9
+ return (
10
+ document.documentElement.getAttribute("data-compact-lists") === "true"
11
+ );
12
+ }
@@ -0,0 +1,14 @@
1
+ import { type Theme } from "@/lib/types/theme";
2
+
3
+ /**
4
+ * Reads the theme currently applied on the document element.
5
+ */
6
+ export function read_document_theme(): Theme {
7
+ if (typeof document === "undefined") {
8
+ return "dark";
9
+ }
10
+
11
+ return document.documentElement.getAttribute("data-theme") === "light"
12
+ ? "light"
13
+ : "dark";
14
+ }
@@ -0,0 +1,26 @@
1
+ import { normalize_stored_tag } from '@/lib/normalize_stored_tag'
2
+ import { read_stored_sheet_tag_filters } from '@/lib/read_stored_sheet_tag_filters'
3
+
4
+ /**
5
+ * Returns stored tag filters for a sheet, normalized and deduplicated.
6
+ */
7
+ export function read_sheet_tag_filter(sheet_name: string): string[] {
8
+ const stored = read_stored_sheet_tag_filters()[sheet_name] ?? []
9
+ const normalized: string[] = []
10
+ const seen = new Set<string>()
11
+
12
+ for (const tag of stored) {
13
+ try {
14
+ const name = normalize_stored_tag(tag)
15
+
16
+ if (!seen.has(name)) {
17
+ seen.add(name)
18
+ normalized.push(name)
19
+ }
20
+ } catch {
21
+ continue
22
+ }
23
+ }
24
+
25
+ return normalized
26
+ }
@@ -0,0 +1,14 @@
1
+ import { ACTIVE_SHEET_STORAGE_KEY } from '@/lib/types/ui_settings'
2
+
3
+ /**
4
+ * Reads the last opened sheet from localStorage (client-only).
5
+ */
6
+ export function read_stored_active_sheet(): string | null {
7
+ try {
8
+ const value = window.localStorage.getItem(ACTIVE_SHEET_STORAGE_KEY)?.trim() ?? ''
9
+
10
+ return value.length > 0 ? value : null
11
+ } catch {
12
+ return null
13
+ }
14
+ }
@@ -0,0 +1,24 @@
1
+ import {
2
+ COMPACT_LISTS_STORAGE_KEY,
3
+ } from "@/lib/types/ui_settings";
4
+
5
+ /**
6
+ * Reads the persisted compact lists setting from localStorage.
7
+ */
8
+ export function read_stored_compact_lists(): boolean | null {
9
+ if (typeof window === "undefined") {
10
+ return null;
11
+ }
12
+
13
+ const stored = window.localStorage.getItem(COMPACT_LISTS_STORAGE_KEY);
14
+
15
+ if (stored === "true") {
16
+ return true;
17
+ }
18
+
19
+ if (stored === "false") {
20
+ return false;
21
+ }
22
+
23
+ return null;
24
+ }
@@ -0,0 +1,16 @@
1
+ import { DEFAULT_SHEET_FIXED_NAME_STORAGE_KEY } from '@/lib/types/ui_settings'
2
+
3
+ /**
4
+ * Reads the fixed default sheet name from localStorage.
5
+ */
6
+ export function read_stored_default_sheet_fixed_name(): string | null {
7
+ try {
8
+ const value =
9
+ window.localStorage.getItem(DEFAULT_SHEET_FIXED_NAME_STORAGE_KEY)?.trim() ??
10
+ ''
11
+
12
+ return value.length > 0 ? value : null
13
+ } catch {
14
+ return null
15
+ }
16
+ }
@@ -0,0 +1,18 @@
1
+ import { parse_default_sheet_session_mode } from '@/lib/parse_default_sheet_session_mode'
2
+ import {
3
+ DEFAULT_SHEET_SESSION_MODE_STORAGE_KEY,
4
+ type DefaultSheetSessionMode,
5
+ } from '@/lib/types/ui_settings'
6
+
7
+ /**
8
+ * Reads the default sheet session mode from localStorage.
9
+ */
10
+ export function read_stored_default_sheet_session_mode(): DefaultSheetSessionMode {
11
+ try {
12
+ const value = window.localStorage.getItem(DEFAULT_SHEET_SESSION_MODE_STORAGE_KEY)
13
+
14
+ return parse_default_sheet_session_mode(value)
15
+ } catch {
16
+ return parse_default_sheet_session_mode(null)
17
+ }
18
+ }