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,118 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+
5
+ import { get_input_class_name } from '@/lib/get_input_class_name'
6
+ import { read_stored_default_sheet_fixed_name } from '@/lib/read_stored_default_sheet_fixed_name'
7
+ import { read_stored_default_sheet_session_mode } from '@/lib/read_stored_default_sheet_session_mode'
8
+ import { notify_settings_saved } from '@/lib/notify_settings_saved'
9
+ import { set_default_sheet_fixed_name } from '@/lib/set_default_sheet_fixed_name'
10
+ import { set_default_sheet_session_mode } from '@/lib/set_default_sheet_session_mode'
11
+ import { type DefaultSheetSessionMode } from '@/lib/types/ui_settings'
12
+
13
+ interface DefaultSheetSessionSettingProps {
14
+ sheet_names: string[]
15
+ }
16
+
17
+ const mode_options: {
18
+ value: DefaultSheetSessionMode
19
+ label: string
20
+ description: string
21
+ }[] = [
22
+ {
23
+ value: 'last_viewed',
24
+ label: 'Last viewed',
25
+ description: 'Open the sheet you were viewing when you last used the tracker.',
26
+ },
27
+ {
28
+ value: 'active_timer',
29
+ label: 'Sheet with active timer',
30
+ description: 'Open the sheet that has a running timer, when one exists.',
31
+ },
32
+ {
33
+ value: 'fixed',
34
+ label: 'Specific sheet',
35
+ description: 'Always open a chosen sheet when you start a new session.',
36
+ },
37
+ ]
38
+
39
+ /**
40
+ * Configures which sheet loads on a new session.
41
+ */
42
+ export function DefaultSheetSessionSetting({
43
+ sheet_names,
44
+ }: DefaultSheetSessionSettingProps) {
45
+ const [mode, set_mode] = useState<DefaultSheetSessionMode>('last_viewed')
46
+ const [fixed_sheet_name, set_fixed_sheet_name] = useState('')
47
+
48
+ useEffect(() => {
49
+ set_mode(read_stored_default_sheet_session_mode())
50
+ const stored_fixed_name = read_stored_default_sheet_fixed_name()
51
+ const fallback_name = sheet_names[0] ?? ''
52
+
53
+ set_fixed_sheet_name(stored_fixed_name ?? fallback_name)
54
+ }, [sheet_names])
55
+
56
+ const handle_mode_change = (next_mode: DefaultSheetSessionMode): void => {
57
+ set_mode(next_mode)
58
+ set_default_sheet_session_mode(next_mode)
59
+
60
+ if (next_mode === 'fixed' && fixed_sheet_name.length > 0) {
61
+ set_default_sheet_fixed_name(fixed_sheet_name)
62
+ }
63
+
64
+ notify_settings_saved()
65
+ }
66
+
67
+ const handle_fixed_sheet_change = (sheet_name: string): void => {
68
+ set_fixed_sheet_name(sheet_name)
69
+ set_default_sheet_fixed_name(sheet_name)
70
+ notify_settings_saved()
71
+ }
72
+
73
+ return (
74
+ <fieldset className="m-0 flex w-full flex-col gap-3 border-0 p-0">
75
+ <legend className="mb-2 text-[0.95rem] font-semibold">
76
+ Default sheet on new session
77
+ </legend>
78
+ <ul className="m-0 flex list-none flex-col gap-2 p-0">
79
+ {mode_options.map((option) => (
80
+ <li key={option.value}>
81
+ <label className="flex w-full cursor-pointer items-start gap-2.5">
82
+ <input
83
+ type="radio"
84
+ name="default-sheet-session-mode"
85
+ className="mt-0.5 shrink-0"
86
+ checked={mode === option.value}
87
+ onChange={() => handle_mode_change(option.value)}
88
+ />
89
+ <span className="flex flex-col gap-0.5">
90
+ <span className="text-[0.9rem] font-semibold">{option.label}</span>
91
+ <span className="text-[0.8rem] leading-snug text-muted">
92
+ {option.description}
93
+ </span>
94
+ </span>
95
+ </label>
96
+ </li>
97
+ ))}
98
+ </ul>
99
+ {mode === 'fixed' ? (
100
+ <label className="flex flex-col gap-1 text-[0.82rem] text-muted">
101
+ Sheet
102
+ <select
103
+ className={get_input_class_name('compact')}
104
+ value={fixed_sheet_name}
105
+ disabled={sheet_names.length === 0}
106
+ onChange={(event) => handle_fixed_sheet_change(event.target.value)}
107
+ >
108
+ {sheet_names.map((sheet_name) => (
109
+ <option key={sheet_name} value={sheet_name}>
110
+ {sheet_name}
111
+ </option>
112
+ ))}
113
+ </select>
114
+ </label>
115
+ ) : null}
116
+ </fieldset>
117
+ )
118
+ }
@@ -0,0 +1,75 @@
1
+ import { ColorPaletteSetting } from '@/components/color-palette-setting'
2
+ import { CheckInFormCollapsedSetting } from '@/components/check-in-form-collapsed-setting'
3
+ import { CompactListsSetting } from '@/components/compact-lists-setting'
4
+ import { DefaultReportingRangeSetting } from '@/components/default-reporting-range-setting'
5
+ import { DefaultReportingSortSetting } from '@/components/default-reporting-sort-setting'
6
+ import { DurationFormatSetting } from '@/components/duration-format-setting'
7
+ import { EntryListSortSetting } from '@/components/entry-list-sort-setting'
8
+ import { SettingsPageLayout } from '@/components/settings-page-layout'
9
+ import { TagFilterModeSetting } from '@/components/tag-filter-mode-setting'
10
+ import { ThemeModeSetting } from '@/components/theme-mode-setting'
11
+ import { TimeFormatSetting } from '@/components/time-format-setting'
12
+ import { TimerInTitleSetting } from '@/components/timer-in-title-setting'
13
+ import { TimerShowSecondsSetting } from '@/components/timer-show-seconds-setting'
14
+ import { WeekStartsOnSetting } from '@/components/week-starts-on-setting'
15
+
16
+ /**
17
+ * Settings page: display, layout, and formatting preferences.
18
+ */
19
+ export function DisplaySettingsView() {
20
+ return (
21
+ <SettingsPageLayout
22
+ breadcrumb={{
23
+ current: 'Display & layout',
24
+ parent: { label: 'Settings', href: '/settings' },
25
+ }}
26
+ title="Display & layout"
27
+ description="How things look across the tracker, reporting, and entry lists."
28
+ >
29
+ <ul
30
+ className="m-0 flex w-full list-none flex-col gap-2 p-0"
31
+ aria-label="Display settings"
32
+ >
33
+ <li className="rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
34
+ <ThemeModeSetting />
35
+ </li>
36
+ <li className="rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
37
+ <ColorPaletteSetting />
38
+ </li>
39
+ <li className="rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
40
+ <CompactListsSetting />
41
+ </li>
42
+ <li className="rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
43
+ <CheckInFormCollapsedSetting />
44
+ </li>
45
+ <li className="rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
46
+ <TimeFormatSetting />
47
+ </li>
48
+ <li className="rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
49
+ <DurationFormatSetting />
50
+ </li>
51
+ <li className="rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
52
+ <TimerShowSecondsSetting />
53
+ </li>
54
+ <li className="rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
55
+ <TimerInTitleSetting />
56
+ </li>
57
+ <li className="rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
58
+ <EntryListSortSetting />
59
+ </li>
60
+ <li className="rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
61
+ <TagFilterModeSetting />
62
+ </li>
63
+ <li className="rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
64
+ <WeekStartsOnSetting />
65
+ </li>
66
+ <li className="rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
67
+ <DefaultReportingSortSetting />
68
+ </li>
69
+ <li className="rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
70
+ <DefaultReportingRangeSetting />
71
+ </li>
72
+ </ul>
73
+ </SettingsPageLayout>
74
+ )
75
+ }
@@ -0,0 +1,40 @@
1
+ 'use client'
2
+
3
+ import { useSyncExternalStore } from 'react'
4
+
5
+ import { SettingRadioGroup } from '@/components/setting-radio-group'
6
+ import { duration_format_preference } from '@/lib/preferences/duration_format_preference'
7
+ import { persist_ui_preference } from '@/lib/persist_ui_preference'
8
+ import { type DurationFormat } from '@/lib/types/ui_preferences'
9
+
10
+ const options: { value: DurationFormat; label: string; description: string }[] = [
11
+ { value: 'humanized', label: 'Humanized', description: 'e.g. 1 hour 25 minutes' },
12
+ { value: 'clock', label: 'Clock', description: 'e.g. 01:25:00' },
13
+ { value: 'decimal', label: 'Decimal hours', description: 'e.g. 1.42h' },
14
+ ]
15
+
16
+ const set_duration_format = (value: DurationFormat): void => {
17
+ persist_ui_preference(duration_format_preference, value)
18
+ }
19
+
20
+ /**
21
+ * Setting: how to display durations across the app.
22
+ */
23
+ export function DurationFormatSetting() {
24
+ const value = useSyncExternalStore(
25
+ duration_format_preference.subscribe,
26
+ duration_format_preference.get_snapshot,
27
+ duration_format_preference.get_server_snapshot,
28
+ )
29
+
30
+ return (
31
+ <SettingRadioGroup<DurationFormat>
32
+ name="duration-format"
33
+ legend="Duration format"
34
+ description="Applies to entry durations, reporting totals, and the active timer."
35
+ value={value}
36
+ options={options}
37
+ on_change={set_duration_format}
38
+ />
39
+ )
40
+ }
@@ -0,0 +1,207 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useState } from 'react'
4
+
5
+ import { HamburgerIcon } from '@/components/hamburger-icon'
6
+ import { prompt_entry_note } from '@/lib/prompt_entry_note'
7
+ import { type SerializedSheet } from '@/lib/types/tracker_state'
8
+
9
+ interface EntryActionsMenuProps {
10
+ current_sheet_name: string
11
+ sheets: SerializedSheet[]
12
+ is_pending: boolean
13
+ on_edit: () => void
14
+ on_add_note?: (text: string) => void
15
+ on_show_add_note_form?: () => void
16
+ on_resume?: () => void
17
+ entry_is_active?: boolean
18
+ on_delete: () => void
19
+ on_move: (target_sheet_name: string) => void
20
+ }
21
+
22
+ const menu_item_class =
23
+ 'block w-full cursor-pointer rounded-[0.45rem] border-0 bg-transparent px-2.5 py-1.5 text-left font-inherit text-[0.85rem] text-inherit hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-55'
24
+
25
+ /**
26
+ * Hamburger menu for entry edit, move, and delete actions.
27
+ */
28
+ export function EntryActionsMenu({
29
+ current_sheet_name,
30
+ sheets,
31
+ is_pending,
32
+ on_edit,
33
+ on_add_note,
34
+ on_show_add_note_form,
35
+ on_resume,
36
+ entry_is_active = false,
37
+ on_delete,
38
+ on_move,
39
+ }: EntryActionsMenuProps) {
40
+ const current_sheet = sheets.find((sheet) => sheet.name === current_sheet_name)
41
+ const resume_blocked =
42
+ entry_is_active ||
43
+ (current_sheet?.hasActiveEntry === true && !entry_is_active)
44
+ const resume_blocked_reason = entry_is_active
45
+ ? 'This entry is already active'
46
+ : 'Another entry is active on this sheet'
47
+ const other_sheets = sheets.filter(
48
+ (sheet) => sheet.name !== current_sheet_name,
49
+ )
50
+ const [is_open, set_is_open] = useState(false)
51
+ const menu_ref = useRef<HTMLDivElement>(null)
52
+
53
+ useEffect(() => {
54
+ if (!is_open) {
55
+ return
56
+ }
57
+
58
+ const handle_pointer_down = (event: PointerEvent): void => {
59
+ if (
60
+ menu_ref.current !== null &&
61
+ !menu_ref.current.contains(event.target as Node)
62
+ ) {
63
+ set_is_open(false)
64
+ }
65
+ }
66
+
67
+ document.addEventListener('pointerdown', handle_pointer_down)
68
+
69
+ return () => {
70
+ document.removeEventListener('pointerdown', handle_pointer_down)
71
+ }
72
+ }, [is_open])
73
+
74
+ const close_menu = (): void => {
75
+ set_is_open(false)
76
+ }
77
+
78
+ return (
79
+ <div className="relative shrink-0" ref={menu_ref}>
80
+ <button
81
+ type="button"
82
+ className="inline-flex cursor-pointer appearance-none items-center justify-center rounded-none border-0 bg-transparent p-0.5 text-muted shadow-none hover:opacity-75 focus-visible:outline-2 focus-visible:outline-input-focus-border focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-55"
83
+ aria-label="Entry actions"
84
+ aria-expanded={is_open}
85
+ aria-haspopup="menu"
86
+ disabled={is_pending}
87
+ onClick={() => set_is_open((open) => !open)}
88
+ >
89
+ <HamburgerIcon />
90
+ </button>
91
+ {is_open ? (
92
+ <ul
93
+ className="absolute right-0 top-full z-10 mt-1.5 min-w-56 list-none rounded-md border border-panel-border bg-panel p-1.5 shadow-md"
94
+ role="menu"
95
+ >
96
+ <li role="none">
97
+ <button
98
+ type="button"
99
+ className={menu_item_class}
100
+ role="menuitem"
101
+ disabled={is_pending}
102
+ onClick={() => {
103
+ close_menu()
104
+ on_edit()
105
+ }}
106
+ >
107
+ Edit times
108
+ </button>
109
+ </li>
110
+ {on_show_add_note_form !== undefined || on_add_note !== undefined ? (
111
+ <li role="none">
112
+ <button
113
+ type="button"
114
+ className={menu_item_class}
115
+ role="menuitem"
116
+ disabled={is_pending}
117
+ onClick={() => {
118
+ close_menu()
119
+
120
+ if (on_show_add_note_form !== undefined) {
121
+ on_show_add_note_form()
122
+ return
123
+ }
124
+
125
+ const text = prompt_entry_note()
126
+
127
+ if (text !== null && on_add_note !== undefined) {
128
+ on_add_note(text)
129
+ }
130
+ }}
131
+ >
132
+ Add note
133
+ </button>
134
+ </li>
135
+ ) : null}
136
+ {on_resume !== undefined ? (
137
+ <li role="none">
138
+ <button
139
+ type="button"
140
+ className={menu_item_class}
141
+ role="menuitem"
142
+ disabled={is_pending || resume_blocked}
143
+ title={resume_blocked ? resume_blocked_reason : undefined}
144
+ onClick={() => {
145
+ close_menu()
146
+ on_resume()
147
+ }}
148
+ >
149
+ Resume
150
+ </button>
151
+ </li>
152
+ ) : null}
153
+ <li className="my-1 border-t border-panel-border" role="separator" aria-hidden="true" />
154
+ <li role="none">
155
+ <p className="m-0 px-2.5 py-0.5 text-[0.72rem] font-semibold uppercase tracking-[0.04em] text-muted">
156
+ Move to sheet
157
+ </p>
158
+ </li>
159
+ {other_sheets.length === 0 ? (
160
+ <li role="none">
161
+ <button type="button" className={menu_item_class} role="menuitem" disabled>
162
+ No other sheets
163
+ </button>
164
+ </li>
165
+ ) : (
166
+ other_sheets.map((sheet) => (
167
+ <li key={sheet.name} role="none">
168
+ <button
169
+ type="button"
170
+ className={`${menu_item_class} pl-4`}
171
+ role="menuitem"
172
+ disabled={is_pending || sheet.hasActiveEntry}
173
+ title={
174
+ sheet.hasActiveEntry
175
+ ? 'That sheet already has an active entry'
176
+ : undefined
177
+ }
178
+ onClick={() => {
179
+ close_menu()
180
+ on_move(sheet.name)
181
+ }}
182
+ >
183
+ {sheet.name}
184
+ </button>
185
+ </li>
186
+ ))
187
+ )}
188
+ <li className="my-1 border-t border-panel-border" role="separator" aria-hidden="true" />
189
+ <li role="none">
190
+ <button
191
+ type="button"
192
+ className={`${menu_item_class} text-danger`}
193
+ role="menuitem"
194
+ disabled={is_pending}
195
+ onClick={() => {
196
+ close_menu()
197
+ on_delete()
198
+ }}
199
+ >
200
+ Delete
201
+ </button>
202
+ </li>
203
+ </ul>
204
+ ) : null}
205
+ </div>
206
+ )
207
+ }
@@ -0,0 +1,113 @@
1
+ 'use client'
2
+
3
+ import { type FormEvent, useState } from 'react'
4
+
5
+ import { format_datetime_hint } from '@/components/format_datetime_hint'
6
+ import { get_button_class_name } from '@/lib/get_button_class_name'
7
+ import { get_input_class_name } from '@/lib/get_input_class_name'
8
+ import { type SerializedEntry } from '@/lib/types/tracker_state'
9
+
10
+ export interface EntryEditFormValues {
11
+ start?: string
12
+ end?: string
13
+ }
14
+
15
+ interface EntryEditFormProps {
16
+ entry: SerializedEntry
17
+ is_pending: boolean
18
+ in_active_panel?: boolean
19
+ on_save: (values: EntryEditFormValues) => void
20
+ on_cancel: () => void
21
+ }
22
+
23
+ /**
24
+ * Form for editing an entry's start and end times with natural language.
25
+ */
26
+ export function EntryEditForm({
27
+ entry,
28
+ is_pending,
29
+ in_active_panel = false,
30
+ on_save,
31
+ on_cancel,
32
+ }: EntryEditFormProps) {
33
+ const [start, set_start] = useState('')
34
+ const [end, set_end] = useState('')
35
+
36
+ const handle_submit = (event: FormEvent<HTMLFormElement>): void => {
37
+ event.preventDefault()
38
+
39
+ const trimmed_start = start.trim()
40
+ const trimmed_end = end.trim()
41
+
42
+ if (trimmed_start.length === 0 && trimmed_end.length === 0) {
43
+ return
44
+ }
45
+
46
+ on_save({
47
+ ...(trimmed_start.length > 0 ? { start: trimmed_start } : {}),
48
+ ...(trimmed_end.length > 0 ? { end: trimmed_end } : {}),
49
+ })
50
+ }
51
+
52
+ const end_hint = entry.isActive
53
+ ? 'still running'
54
+ : format_datetime_hint(entry.end ?? entry.start)
55
+
56
+ return (
57
+ <form
58
+ className={`flex flex-col gap-3 rounded-[0.65rem] border border-panel-border bg-input-bg p-3 ${in_active_panel ? 'mt-2' : ''}`}
59
+ onSubmit={handle_submit}
60
+ >
61
+ <p className="m-0 text-[0.85rem] font-semibold">Edit times</p>
62
+ <div className="grid grid-cols-2 gap-2.5 max-[860px]:grid-cols-1">
63
+ <label className="flex flex-col gap-1" htmlFor={`entry-start-${entry.id}`}>
64
+ <span className="text-[0.8rem] font-semibold">Start</span>
65
+ <span className="text-[0.72rem] text-muted">
66
+ Current: {format_datetime_hint(entry.start)}
67
+ </span>
68
+ <input
69
+ id={`entry-start-${entry.id}`}
70
+ className={get_input_class_name('compact')}
71
+ value={start}
72
+ onChange={(event) => set_start(event.target.value)}
73
+ placeholder="e.g. 10am, 30 minutes ago"
74
+ disabled={is_pending}
75
+ />
76
+ </label>
77
+ <label className="flex flex-col gap-1" htmlFor={`entry-end-${entry.id}`}>
78
+ <span className="text-[0.8rem] font-semibold">End</span>
79
+ <span className="text-[0.72rem] text-muted">Current: {end_hint}</span>
80
+ <input
81
+ id={`entry-end-${entry.id}`}
82
+ className={get_input_class_name('compact')}
83
+ value={end}
84
+ onChange={(event) => set_end(event.target.value)}
85
+ placeholder={
86
+ entry.isActive ? 'e.g. now, 5 minutes ago' : 'e.g. 11:30am'
87
+ }
88
+ disabled={is_pending}
89
+ />
90
+ </label>
91
+ </div>
92
+ <div className="flex flex-wrap gap-2">
93
+ <button
94
+ type="submit"
95
+ className={get_button_class_name('primary', 'small')}
96
+ disabled={
97
+ is_pending || (start.trim().length === 0 && end.trim().length === 0)
98
+ }
99
+ >
100
+ Save
101
+ </button>
102
+ <button
103
+ type="button"
104
+ className={get_button_class_name('ghost', 'small')}
105
+ disabled={is_pending}
106
+ onClick={on_cancel}
107
+ >
108
+ Cancel
109
+ </button>
110
+ </div>
111
+ </form>
112
+ )
113
+ }
@@ -0,0 +1,128 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+
5
+ import { Checkbox } from '@/components/checkbox'
6
+ import { get_button_class_name } from '@/lib/get_button_class_name'
7
+ import { get_input_class_name } from '@/lib/get_input_class_name'
8
+ import {
9
+ type SerializedEntry,
10
+ type SerializedSheet,
11
+ } from '@/lib/types/tracker_state'
12
+
13
+ interface EntryListBulkBarProps {
14
+ selected_count: number
15
+ total_count: number
16
+ all_selected: boolean
17
+ some_selected: boolean
18
+ selected_entries: SerializedEntry[]
19
+ sheets: SerializedSheet[]
20
+ is_pending: boolean
21
+ on_toggle_all: () => void
22
+ on_move: (target_sheet_name: string) => void
23
+ on_delete: () => void
24
+ on_clear: () => void
25
+ }
26
+
27
+ /**
28
+ * Header takeover for bulk actions when entries are selected.
29
+ */
30
+ export function EntryListBulkBar({
31
+ selected_count,
32
+ total_count,
33
+ all_selected,
34
+ some_selected,
35
+ selected_entries,
36
+ sheets,
37
+ is_pending,
38
+ on_toggle_all,
39
+ on_move,
40
+ on_delete,
41
+ on_clear,
42
+ }: EntryListBulkBarProps) {
43
+ const [target_sheet_name, set_target_sheet_name] = useState('')
44
+ const has_active_selection = selected_entries.some((entry) => entry.isActive)
45
+ const movable_sheets = sheets.filter((sheet) => {
46
+ if (has_active_selection && sheet.hasActiveEntry) {
47
+ return false
48
+ }
49
+
50
+ return selected_entries.some((entry) => entry.sheetName !== sheet.name)
51
+ })
52
+
53
+ const handle_move = (): void => {
54
+ const trimmed = target_sheet_name.trim()
55
+
56
+ if (trimmed.length === 0) {
57
+ return
58
+ }
59
+
60
+ on_move(trimmed)
61
+ set_target_sheet_name('')
62
+ }
63
+
64
+ return (
65
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-2 rounded-md border border-accent-border bg-accent-soft px-3 py-2">
66
+ <Checkbox
67
+ className="shrink-0"
68
+ checked={all_selected}
69
+ indeterminate={some_selected}
70
+ disabled={is_pending}
71
+ aria_label={all_selected ? 'Deselect all entries' : 'Select all entries'}
72
+ on_change={on_toggle_all}
73
+ />
74
+ <p className="m-0 shrink-0 text-[0.85rem] font-semibold">
75
+ {selected_count}
76
+ <span className="text-muted">{` of ${total_count} selected`}</span>
77
+ </p>
78
+ <div className="ml-auto flex flex-wrap items-center gap-2">
79
+ <div className="flex items-center gap-1.5">
80
+ <select
81
+ className={`${get_input_class_name('compact')} min-w-32`}
82
+ value={target_sheet_name}
83
+ disabled={is_pending || movable_sheets.length === 0}
84
+ aria-label="Move to sheet"
85
+ onChange={(event) => set_target_sheet_name(event.target.value)}
86
+ >
87
+ <option value="">Move to…</option>
88
+ {movable_sheets.map((sheet) => (
89
+ <option key={sheet.name} value={sheet.name}>
90
+ {sheet.name}
91
+ </option>
92
+ ))}
93
+ </select>
94
+ <button
95
+ type="button"
96
+ className={get_button_class_name('ghost', 'small')}
97
+ disabled={
98
+ is_pending ||
99
+ target_sheet_name.trim().length === 0 ||
100
+ movable_sheets.length === 0
101
+ }
102
+ onClick={handle_move}
103
+ >
104
+ Move
105
+ </button>
106
+ </div>
107
+ <button
108
+ type="button"
109
+ className={get_button_class_name('danger', 'small')}
110
+ disabled={is_pending}
111
+ onClick={on_delete}
112
+ >
113
+ Delete
114
+ </button>
115
+ <button
116
+ type="button"
117
+ className={`${get_button_class_name('ghost', 'small')} shrink-0`}
118
+ disabled={is_pending}
119
+ aria-label="Clear selection"
120
+ title="Clear selection"
121
+ onClick={on_clear}
122
+ >
123
+
124
+ </button>
125
+ </div>
126
+ </div>
127
+ )
128
+ }