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.
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +28 -0
- package/CLAUDE.md +1 -0
- package/README.md +36 -0
- package/app/api/backup/route.ts +39 -0
- package/app/api/entry/delete-bulk/route.ts +53 -0
- package/app/api/entry/move/route.ts +46 -0
- package/app/api/entry/move-bulk/route.ts +62 -0
- package/app/api/entry/route.ts +75 -0
- package/app/api/in/route.ts +38 -0
- package/app/api/note/route.ts +120 -0
- package/app/api/out/route.ts +31 -0
- package/app/api/sheet/route.ts +68 -0
- package/app/api/state/route.ts +16 -0
- package/app/api/tags/route.ts +75 -0
- package/app/color-palettes.css +260 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +140 -0
- package/app/layout.tsx +54 -0
- package/app/page.tsx +24 -0
- package/app/reporting/page.tsx +11 -0
- package/app/settings/data/page.tsx +9 -0
- package/app/settings/display/page.tsx +8 -0
- package/app/settings/page.tsx +12 -0
- package/app/settings/tags/page.tsx +13 -0
- package/bin/stt-ui.js +63 -0
- package/components/active-entry-panel.tsx +199 -0
- package/components/backup-restore-setting.tsx +168 -0
- package/components/check-in-form-collapsed-setting.tsx +44 -0
- package/components/check-in-form-collapsible.tsx +52 -0
- package/components/check-in-form.tsx +89 -0
- package/components/checkbox.tsx +75 -0
- package/components/checkout-button-group.tsx +73 -0
- package/components/chevron-icon.tsx +25 -0
- package/components/clear-tag-filters-on-sheet-change-setting.tsx +45 -0
- package/components/color-palette-setting.tsx +75 -0
- package/components/compact-lists-setting.tsx +42 -0
- package/components/confirm-before-checkout-setting.tsx +42 -0
- package/components/confirm-destructive-actions-setting.tsx +46 -0
- package/components/confirm-dialog-provider.tsx +71 -0
- package/components/confirm-dialog.tsx +90 -0
- package/components/data-settings-view.tsx +47 -0
- package/components/default-reporting-range-setting.tsx +56 -0
- package/components/default-reporting-sort-setting.tsx +45 -0
- package/components/default-sheet-session-setting.tsx +118 -0
- package/components/display-settings-view.tsx +75 -0
- package/components/duration-format-setting.tsx +40 -0
- package/components/entry-actions-menu.tsx +207 -0
- package/components/entry-edit-form.tsx +113 -0
- package/components/entry-list-bulk-bar.tsx +128 -0
- package/components/entry-list-sort-setting.tsx +41 -0
- package/components/entry-list.tsx +336 -0
- package/components/entry-notes-list.tsx +211 -0
- package/components/entry-tag-filter.tsx +99 -0
- package/components/format_datetime_hint.ts +8 -0
- package/components/format_time.ts +10 -0
- package/components/general-settings-view.tsx +40 -0
- package/components/hamburger-icon.tsx +21 -0
- package/components/note-edit-form.tsx +77 -0
- package/components/note-form.tsx +109 -0
- package/components/pencil-icon.tsx +21 -0
- package/components/reporting-date-range-picker.tsx +121 -0
- package/components/reporting-sort-controls.tsx +53 -0
- package/components/reporting-view.tsx +340 -0
- package/components/setting-radio-group.tsx +79 -0
- package/components/settings-nav.tsx +66 -0
- package/components/settings-page-layout.tsx +53 -0
- package/components/settings-saved-toast.tsx +57 -0
- package/components/sheet-actions-menu.tsx +108 -0
- package/components/sheet-sidebar.tsx +196 -0
- package/components/tag-autocomplete-input.tsx +183 -0
- package/components/tag-filter-mode-setting.tsx +47 -0
- package/components/tag-management-view.tsx +290 -0
- package/components/theme-mode-setting.tsx +44 -0
- package/components/theme-mode-system-listener.tsx +43 -0
- package/components/theme_switcher.tsx +38 -0
- package/components/time-format-setting.tsx +39 -0
- package/components/timer-in-title-setting.tsx +38 -0
- package/components/timer-show-seconds-setting.tsx +41 -0
- package/components/tracker-active-bar.tsx +76 -0
- package/components/tracker-app.tsx +338 -0
- package/components/tracker-breadcrumb.tsx +56 -0
- package/components/tracker-document-title.tsx +67 -0
- package/components/tracker-topbar.tsx +63 -0
- package/components/trash-icon.tsx +24 -0
- package/components/week-starts-on-setting.tsx +39 -0
- package/eslint.config.mjs +18 -0
- package/lib/add_note_to_entry.ts +65 -0
- package/lib/api_error_response.ts +10 -0
- package/lib/apply_accent_color.ts +12 -0
- package/lib/apply_color_palette.ts +12 -0
- package/lib/apply_compact_lists.ts +9 -0
- package/lib/apply_tag_autocomplete_selection.ts +26 -0
- package/lib/apply_theme.ts +8 -0
- package/lib/build_reporting_stats.ts +55 -0
- package/lib/build_resume_description.ts +15 -0
- package/lib/check_in_entry.ts +81 -0
- package/lib/check_out_entry.ts +75 -0
- package/lib/collect_known_tags.ts +22 -0
- package/lib/collect_tag_stats.ts +27 -0
- package/lib/collect_tags_from_entries.ts +35 -0
- package/lib/config.ts +9 -0
- package/lib/convert_json_db.ts +49 -0
- package/lib/delete_entries.ts +62 -0
- package/lib/delete_entry.ts +29 -0
- package/lib/delete_note_on_entry.ts +42 -0
- package/lib/delete_sheet.ts +30 -0
- package/lib/delete_tracker_action.ts +22 -0
- package/lib/edit_entry.ts +56 -0
- package/lib/edit_note_on_entry.ts +49 -0
- package/lib/ensure_dir_exists.ts +22 -0
- package/lib/entry_matches_tag_filter.ts +26 -0
- package/lib/fetch_tracker_state.ts +15 -0
- package/lib/filter_entries_by_tags.ts +20 -0
- package/lib/filter_known_tags.ts +20 -0
- package/lib/find_all_serialized_active_entries.ts +28 -0
- package/lib/find_serialized_active_entry.ts +12 -0
- package/lib/find_serialized_active_entry_for_sheet.ts +31 -0
- package/lib/find_sheet_with_active_entry.ts +16 -0
- package/lib/format_display_tag.ts +6 -0
- package/lib/format_duration.ts +45 -0
- package/lib/gen_db.ts +43 -0
- package/lib/get_active_panel_class_name.ts +20 -0
- package/lib/get_average_entry_ms.ts +13 -0
- package/lib/get_button_class_name.ts +24 -0
- package/lib/get_check_out_confirm_dialog.ts +19 -0
- package/lib/get_clipped_entry_duration_ms.ts +18 -0
- package/lib/get_compact_lists_snapshot.ts +15 -0
- package/lib/get_date_range_ms_from_inputs.ts +31 -0
- package/lib/get_delete_entries_confirm_dialog.ts +21 -0
- package/lib/get_delete_entry_confirm_dialog.ts +19 -0
- package/lib/get_delete_note_confirm_dialog.ts +21 -0
- package/lib/get_delete_sheet_confirm_dialog.ts +25 -0
- package/lib/get_entry_duration_ms.ts +14 -0
- package/lib/get_entry_row_key.ts +8 -0
- package/lib/get_initial_preferred_sheet_name.ts +34 -0
- package/lib/get_initial_reporting_range_inputs.ts +31 -0
- package/lib/get_input_class_name.ts +15 -0
- package/lib/get_merge_tags_confirm_dialog.ts +25 -0
- package/lib/get_period_range_ms.ts +43 -0
- package/lib/get_reporting_date_range_shortcut_inputs.ts +84 -0
- package/lib/get_reporting_period_totals.ts +39 -0
- package/lib/get_reporting_stats.ts +25 -0
- package/lib/get_restore_db_confirm_dialog.ts +14 -0
- package/lib/get_running_entry_key.ts +8 -0
- package/lib/get_serialized_entries_total_ms.ts +10 -0
- package/lib/get_sheet.ts +14 -0
- package/lib/get_sheet_report_stats.ts +22 -0
- package/lib/get_sheet_report_stats_for_range.ts +46 -0
- package/lib/get_sheet_tag_filter_snapshot.ts +22 -0
- package/lib/get_sheets_duration_in_range.ts +27 -0
- package/lib/get_tag_autocomplete_context.ts +32 -0
- package/lib/get_theme_snapshot.ts +16 -0
- package/lib/get_tracker_state.ts +67 -0
- package/lib/has_string_value.ts +6 -0
- package/lib/is_entry_in_day.ts +15 -0
- package/lib/is_idle_sheet_report.ts +8 -0
- package/lib/is_json_time_tracker_db.ts +14 -0
- package/lib/merge_tags_across_db.ts +79 -0
- package/lib/migrate_json_db.ts +56 -0
- package/lib/migrate_json_db_to_version_three.ts +51 -0
- package/lib/migrate_json_db_to_version_two.ts +50 -0
- package/lib/move_entries_to_sheet.ts +152 -0
- package/lib/move_entry_to_sheet.ts +82 -0
- package/lib/normalize_stored_tag.ts +16 -0
- package/lib/notify_settings_saved.ts +47 -0
- package/lib/parse_default_sheet_session_mode.ts +21 -0
- package/lib/parse_entry_from_input.ts +23 -0
- package/lib/parse_natural_language_date.ts +23 -0
- package/lib/parse_reporting_source_sheets.ts +22 -0
- package/lib/partition_sheet_report_stats.ts +30 -0
- package/lib/patch_tracker_action.ts +22 -0
- package/lib/persist_ui_preference.ts +18 -0
- package/lib/post_tracker_action.ts +22 -0
- package/lib/preferences/accent_color_preference.ts +21 -0
- package/lib/preferences/check_in_form_collapsed_preference.ts +20 -0
- package/lib/preferences/clear_tag_filters_on_sheet_change_preference.ts +20 -0
- package/lib/preferences/color_palette_preference.ts +21 -0
- package/lib/preferences/confirm_before_checkout_preference.ts +20 -0
- package/lib/preferences/confirm_destructive_actions_preference.ts +20 -0
- package/lib/preferences/default_reporting_range_preference.ts +21 -0
- package/lib/preferences/default_reporting_sort_preference.ts +24 -0
- package/lib/preferences/duration_format_preference.ts +19 -0
- package/lib/preferences/entry_list_sort_preference.ts +21 -0
- package/lib/preferences/tag_filter_mode_preference.ts +18 -0
- package/lib/preferences/theme_mode_preference.ts +18 -0
- package/lib/preferences/time_format_preference.ts +18 -0
- package/lib/preferences/timer_in_title_preference.ts +18 -0
- package/lib/preferences/timer_show_seconds_preference.ts +19 -0
- package/lib/preferences/week_starts_on_preference.ts +19 -0
- package/lib/prompt_check_out_at.ts +17 -0
- package/lib/prompt_entry_note.ts +14 -0
- package/lib/prune_sheet_tag_filter.ts +27 -0
- package/lib/read_db.ts +49 -0
- package/lib/read_db_backup_contents.ts +22 -0
- package/lib/read_document_compact_lists.ts +12 -0
- package/lib/read_document_theme.ts +14 -0
- package/lib/read_sheet_tag_filter.ts +26 -0
- package/lib/read_stored_active_sheet.ts +14 -0
- package/lib/read_stored_compact_lists.ts +24 -0
- package/lib/read_stored_default_sheet_fixed_name.ts +16 -0
- package/lib/read_stored_default_sheet_session_mode.ts +18 -0
- package/lib/read_stored_sheet_tag_filters.ts +28 -0
- package/lib/read_stored_theme.ts +18 -0
- package/lib/rename_sheet.ts +39 -0
- package/lib/rename_tag_across_db.ts +19 -0
- package/lib/resolve_active_sheet_name.ts +36 -0
- package/lib/resolve_session_preferred_sheet.ts +37 -0
- package/lib/resolve_theme.ts +18 -0
- package/lib/resolve_theme_mode_to_theme.ts +19 -0
- package/lib/restore_db_from_uploaded_json.ts +24 -0
- package/lib/serialize_entry.ts +27 -0
- package/lib/serialize_reporting_source_sheets.ts +19 -0
- package/lib/serialize_sheet_entries.ts +18 -0
- package/lib/set_accent_color.ts +12 -0
- package/lib/set_active_sheet.ts +18 -0
- package/lib/set_color_palette.ts +12 -0
- package/lib/set_compact_lists.ts +12 -0
- package/lib/set_default_sheet_fixed_name.ts +8 -0
- package/lib/set_default_sheet_session_mode.ts +11 -0
- package/lib/set_sheet_tag_filter.ts +13 -0
- package/lib/set_theme_mode.ts +19 -0
- package/lib/sheet_tag_filter_snapshots.ts +48 -0
- package/lib/sort_serialized_entries.ts +35 -0
- package/lib/sort_sheet_report_stats.ts +43 -0
- package/lib/subscribe_compact_lists.ts +25 -0
- package/lib/subscribe_sheet_tag_filters.ts +28 -0
- package/lib/subscribe_theme.ts +23 -0
- package/lib/sync_active_sheet_preference.ts +19 -0
- package/lib/tags_are_equal.ts +12 -0
- package/lib/theme_init_script.ts +11 -0
- package/lib/toggle_sheet_tag_filter.ts +28 -0
- package/lib/toggle_theme.ts +20 -0
- package/lib/types/confirm_dialog.ts +9 -0
- package/lib/types/data.ts +16 -0
- package/lib/types/generic_data.ts +25 -0
- package/lib/types/index.ts +2 -0
- package/lib/types/reporting.ts +59 -0
- package/lib/types/tag_management.ts +7 -0
- package/lib/types/theme.ts +3 -0
- package/lib/types/tracker_state.ts +39 -0
- package/lib/types/ui_preferences.ts +104 -0
- package/lib/types/ui_settings.ts +17 -0
- package/lib/ui_preference_store.ts +80 -0
- package/lib/ui_settings_init_script.ts +33 -0
- package/lib/use_check_in_form_collapsed.ts +18 -0
- package/lib/use_clear_tag_filters_on_sheet_change.ts +18 -0
- package/lib/use_confirm_before_checkout.ts +18 -0
- package/lib/use_confirm_destructive_actions.ts +18 -0
- package/lib/use_duration_format.ts +17 -0
- package/lib/use_entry_list_sort.ts +17 -0
- package/lib/use_tag_filter_mode.ts +17 -0
- package/lib/use_time_format.ts +17 -0
- package/lib/use_timer_in_title.ts +18 -0
- package/lib/use_timer_show_seconds.ts +18 -0
- package/lib/use_week_starts_on.ts +17 -0
- package/lib/validate_entry_times.ts +12 -0
- package/lib/week_starts_on_to_index.ts +8 -0
- package/lib/write_active_sheet_preference.ts +28 -0
- package/lib/write_db.ts +20 -0
- package/lib/write_sheet_tag_filter.ts +20 -0
- package/lib/write_stored_compact_lists.ts +15 -0
- package/lib/write_stored_default_sheet_fixed_name.ts +28 -0
- package/lib/write_stored_default_sheet_session_mode.ts +24 -0
- package/lib/write_stored_sheet_tag_filters.ts +18 -0
- package/lib/write_stored_theme.ts +12 -0
- package/next.config.ts +7 -0
- package/package.json +96 -0
- package/pnpm-workspace.yaml +7 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { format } from 'date-fns'
|
|
2
|
+
|
|
3
|
+
import { type TimeFormat } from '@/lib/types/ui_preferences'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Formats an ISO timestamp for display in entry lists.
|
|
7
|
+
*/
|
|
8
|
+
export function format_time(iso: string, time_format: TimeFormat = '12h'): string {
|
|
9
|
+
return format(new Date(iso), time_format === '24h' ? 'HH:mm' : 'h:mm a')
|
|
10
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ClearTagFiltersOnSheetChangeSetting } from '@/components/clear-tag-filters-on-sheet-change-setting'
|
|
2
|
+
import { ConfirmBeforeCheckoutSetting } from '@/components/confirm-before-checkout-setting'
|
|
3
|
+
import { ConfirmDestructiveActionsSetting } from '@/components/confirm-destructive-actions-setting'
|
|
4
|
+
import { DefaultSheetSessionSetting } from '@/components/default-sheet-session-setting'
|
|
5
|
+
import { SettingsPageLayout } from '@/components/settings-page-layout'
|
|
6
|
+
|
|
7
|
+
interface GeneralSettingsViewProps {
|
|
8
|
+
sheet_names: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Settings page: general tracker behavior.
|
|
13
|
+
*/
|
|
14
|
+
export function GeneralSettingsView({ sheet_names }: GeneralSettingsViewProps) {
|
|
15
|
+
return (
|
|
16
|
+
<SettingsPageLayout
|
|
17
|
+
breadcrumb={{ current: 'General', parent: { label: 'Settings' } }}
|
|
18
|
+
title="General"
|
|
19
|
+
description="Tracker startup, confirmations, and session behavior."
|
|
20
|
+
>
|
|
21
|
+
<ul
|
|
22
|
+
className="m-0 flex w-full list-none flex-col gap-2 p-0"
|
|
23
|
+
aria-label="General settings"
|
|
24
|
+
>
|
|
25
|
+
<li className="rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
|
|
26
|
+
<DefaultSheetSessionSetting sheet_names={sheet_names} />
|
|
27
|
+
</li>
|
|
28
|
+
<li className="rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
|
|
29
|
+
<ConfirmBeforeCheckoutSetting />
|
|
30
|
+
</li>
|
|
31
|
+
<li className="rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
|
|
32
|
+
<ConfirmDestructiveActionsSetting />
|
|
33
|
+
</li>
|
|
34
|
+
<li className="rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
|
|
35
|
+
<ClearTagFiltersOnSheetChangeSetting />
|
|
36
|
+
</li>
|
|
37
|
+
</ul>
|
|
38
|
+
</SettingsPageLayout>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a three-line menu icon without a filled background.
|
|
3
|
+
*/
|
|
4
|
+
export function HamburgerIcon() {
|
|
5
|
+
return (
|
|
6
|
+
<svg
|
|
7
|
+
className="block h-4 w-4 text-muted"
|
|
8
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
9
|
+
viewBox="0 0 24 24"
|
|
10
|
+
fill="none"
|
|
11
|
+
stroke="currentColor"
|
|
12
|
+
strokeWidth="2"
|
|
13
|
+
strokeLinecap="round"
|
|
14
|
+
aria-hidden="true"
|
|
15
|
+
>
|
|
16
|
+
<path d="M4 7h16" />
|
|
17
|
+
<path d="M4 12h16" />
|
|
18
|
+
<path d="M4 17h16" />
|
|
19
|
+
</svg>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type FormEvent, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { get_button_class_name } from '@/lib/get_button_class_name'
|
|
6
|
+
import { get_input_class_name } from '@/lib/get_input_class_name'
|
|
7
|
+
|
|
8
|
+
interface NoteEditFormProps {
|
|
9
|
+
initial_text: string
|
|
10
|
+
is_pending: boolean
|
|
11
|
+
inline?: boolean
|
|
12
|
+
on_save: (text: string) => void
|
|
13
|
+
on_cancel: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Inline form for editing an existing entry note.
|
|
18
|
+
*/
|
|
19
|
+
export function NoteEditForm({
|
|
20
|
+
initial_text,
|
|
21
|
+
is_pending,
|
|
22
|
+
inline = false,
|
|
23
|
+
on_save,
|
|
24
|
+
on_cancel,
|
|
25
|
+
}: NoteEditFormProps) {
|
|
26
|
+
const [text, set_text] = useState(initial_text)
|
|
27
|
+
const line_count = initial_text.split('\n').length
|
|
28
|
+
const rows = Math.min(6, Math.max(2, line_count))
|
|
29
|
+
|
|
30
|
+
const handle_submit = (event: FormEvent<HTMLFormElement>): void => {
|
|
31
|
+
event.preventDefault()
|
|
32
|
+
const trimmed = text.trim()
|
|
33
|
+
|
|
34
|
+
if (trimmed.length === 0) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
on_save(trimmed)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const textarea_class = inline
|
|
42
|
+
? `${get_input_class_name()} min-h-10 w-full resize-y text-xs leading-snug`
|
|
43
|
+
: `${get_input_class_name()} min-h-10 w-full resize-y text-[0.85rem] leading-snug`
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<form
|
|
47
|
+
className="flex w-full flex-col gap-1.5"
|
|
48
|
+
onSubmit={handle_submit}
|
|
49
|
+
onClick={(event) => event.stopPropagation()}
|
|
50
|
+
>
|
|
51
|
+
<textarea
|
|
52
|
+
className={textarea_class}
|
|
53
|
+
value={text}
|
|
54
|
+
rows={rows}
|
|
55
|
+
disabled={is_pending}
|
|
56
|
+
onChange={(event) => set_text(event.target.value)}
|
|
57
|
+
/>
|
|
58
|
+
<div className="flex justify-end gap-1.5">
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
className={get_button_class_name('ghost')}
|
|
62
|
+
disabled={is_pending}
|
|
63
|
+
onClick={on_cancel}
|
|
64
|
+
>
|
|
65
|
+
Cancel
|
|
66
|
+
</button>
|
|
67
|
+
<button
|
|
68
|
+
type="submit"
|
|
69
|
+
className={get_button_class_name('ghost')}
|
|
70
|
+
disabled={is_pending || text.trim().length === 0}
|
|
71
|
+
>
|
|
72
|
+
Save
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
</form>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type FormEvent, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { get_button_class_name } from '@/lib/get_button_class_name'
|
|
6
|
+
import { get_input_class_name } from '@/lib/get_input_class_name'
|
|
7
|
+
|
|
8
|
+
interface NoteFormProps {
|
|
9
|
+
on_submit: (text: string, at?: string) => void
|
|
10
|
+
on_cancel?: () => void
|
|
11
|
+
is_pending: boolean
|
|
12
|
+
in_active_panel?: boolean
|
|
13
|
+
in_bar?: boolean
|
|
14
|
+
allow_at?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Adds a note to the active entry.
|
|
19
|
+
*/
|
|
20
|
+
export function NoteForm({
|
|
21
|
+
on_submit,
|
|
22
|
+
on_cancel,
|
|
23
|
+
is_pending,
|
|
24
|
+
in_active_panel = false,
|
|
25
|
+
in_bar = false,
|
|
26
|
+
allow_at = false,
|
|
27
|
+
}: NoteFormProps) {
|
|
28
|
+
const [text, set_text] = useState('')
|
|
29
|
+
const [at, set_at] = useState('')
|
|
30
|
+
|
|
31
|
+
const handle_submit = (event: FormEvent<HTMLFormElement>): void => {
|
|
32
|
+
event.preventDefault()
|
|
33
|
+
const trimmed = text.trim()
|
|
34
|
+
|
|
35
|
+
if (trimmed.length === 0) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const trimmed_at = at.trim()
|
|
40
|
+
|
|
41
|
+
on_submit(trimmed, trimmed_at.length > 0 ? trimmed_at : undefined)
|
|
42
|
+
set_text('')
|
|
43
|
+
set_at('')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const border_class =
|
|
47
|
+
in_active_panel && !in_bar
|
|
48
|
+
? 'border-t border-accent-border pt-4'
|
|
49
|
+
: in_bar
|
|
50
|
+
? 'border-t border-[color-mix(in_srgb,var(--accent-border)_65%,var(--panel-border))] pt-3.5'
|
|
51
|
+
: ''
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<form
|
|
55
|
+
className={`flex flex-col gap-2 ${border_class}`}
|
|
56
|
+
onSubmit={handle_submit}
|
|
57
|
+
>
|
|
58
|
+
<label className="text-[0.85rem] text-muted" htmlFor="note-text">
|
|
59
|
+
Add note
|
|
60
|
+
</label>
|
|
61
|
+
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-2 max-[860px]:grid-cols-1">
|
|
62
|
+
<input
|
|
63
|
+
id="note-text"
|
|
64
|
+
className={get_input_class_name()}
|
|
65
|
+
value={text}
|
|
66
|
+
onChange={(event) => set_text(event.target.value)}
|
|
67
|
+
placeholder="Pair with alice on the widget"
|
|
68
|
+
disabled={is_pending}
|
|
69
|
+
autoFocus
|
|
70
|
+
/>
|
|
71
|
+
<div className="flex flex-wrap items-center gap-2 max-[860px]:w-full">
|
|
72
|
+
<button
|
|
73
|
+
type="submit"
|
|
74
|
+
className={get_button_class_name('ghost')}
|
|
75
|
+
disabled={is_pending || text.trim().length === 0}
|
|
76
|
+
>
|
|
77
|
+
Save note
|
|
78
|
+
</button>
|
|
79
|
+
{on_cancel !== undefined ? (
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
className={get_button_class_name('ghost')}
|
|
83
|
+
disabled={is_pending}
|
|
84
|
+
onClick={on_cancel}
|
|
85
|
+
>
|
|
86
|
+
Cancel
|
|
87
|
+
</button>
|
|
88
|
+
) : null}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
{allow_at ? (
|
|
92
|
+
<>
|
|
93
|
+
<label className="text-[0.85rem] text-muted" htmlFor="note-at">
|
|
94
|
+
Note time{' '}
|
|
95
|
+
<span className="font-normal opacity-85">(optional, natural language)</span>
|
|
96
|
+
</label>
|
|
97
|
+
<input
|
|
98
|
+
id="note-at"
|
|
99
|
+
className={get_input_class_name()}
|
|
100
|
+
value={at}
|
|
101
|
+
onChange={(event) => set_at(event.target.value)}
|
|
102
|
+
placeholder="e.g. 30 minutes ago"
|
|
103
|
+
disabled={is_pending}
|
|
104
|
+
/>
|
|
105
|
+
</>
|
|
106
|
+
) : null}
|
|
107
|
+
</form>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a small pencil icon for edit actions.
|
|
3
|
+
*/
|
|
4
|
+
export function PencilIcon() {
|
|
5
|
+
return (
|
|
6
|
+
<svg
|
|
7
|
+
className="h-[0.85rem] w-[0.85rem]"
|
|
8
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
9
|
+
viewBox="0 0 24 24"
|
|
10
|
+
fill="none"
|
|
11
|
+
stroke="currentColor"
|
|
12
|
+
strokeWidth="2"
|
|
13
|
+
strokeLinecap="round"
|
|
14
|
+
strokeLinejoin="round"
|
|
15
|
+
aria-hidden="true"
|
|
16
|
+
>
|
|
17
|
+
<path d="M12 20h9" />
|
|
18
|
+
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" />
|
|
19
|
+
</svg>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { get_button_class_name } from '@/lib/get_button_class_name'
|
|
4
|
+
import { get_input_class_name } from '@/lib/get_input_class_name'
|
|
5
|
+
import { get_reporting_date_range_shortcut_inputs } from '@/lib/get_reporting_date_range_shortcut_inputs'
|
|
6
|
+
import { use_week_starts_on } from '@/lib/use_week_starts_on'
|
|
7
|
+
import { week_starts_on_to_index } from '@/lib/week_starts_on_to_index'
|
|
8
|
+
import {
|
|
9
|
+
type ReportingDateRangeInputs,
|
|
10
|
+
type ReportingDateRangeShortcut,
|
|
11
|
+
} from '@/lib/types/reporting'
|
|
12
|
+
|
|
13
|
+
export type { ReportingDateRangeInputs } from '@/lib/types/reporting'
|
|
14
|
+
|
|
15
|
+
const shortcut_options: {
|
|
16
|
+
value: ReportingDateRangeShortcut
|
|
17
|
+
label: string
|
|
18
|
+
}[] = [
|
|
19
|
+
{ value: 'today', label: 'Today' },
|
|
20
|
+
{ value: 'yesterday', label: 'Yesterday' },
|
|
21
|
+
{ value: 'week', label: 'This week' },
|
|
22
|
+
{ value: 'month', label: 'This month' },
|
|
23
|
+
{ value: 'last_month', label: 'Last month' },
|
|
24
|
+
{ value: 'year', label: 'This year' },
|
|
25
|
+
{ value: 'last_year', label: 'Last year' },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
interface ReportingDateRangePickerProps {
|
|
29
|
+
range: ReportingDateRangeInputs
|
|
30
|
+
is_invalid: boolean
|
|
31
|
+
on_range_change: (range: ReportingDateRangeInputs) => void
|
|
32
|
+
on_clear: () => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Date range filter controls for the reporting view.
|
|
37
|
+
*/
|
|
38
|
+
export function ReportingDateRangePicker({
|
|
39
|
+
range,
|
|
40
|
+
is_invalid,
|
|
41
|
+
on_range_change,
|
|
42
|
+
on_clear,
|
|
43
|
+
}: ReportingDateRangePickerProps) {
|
|
44
|
+
const week_starts_on = use_week_starts_on()
|
|
45
|
+
const week_starts_on_index = week_starts_on_to_index(week_starts_on)
|
|
46
|
+
const has_filter =
|
|
47
|
+
range.from_date.length > 0 || range.to_date.length > 0
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<fieldset className="m-0 flex w-full max-w-2xl flex-col gap-2 rounded-md border border-panel-border bg-panel p-3.5 shadow-sm">
|
|
51
|
+
<legend className="mb-2 text-[0.72rem] font-semibold uppercase tracking-[0.06em] text-muted">
|
|
52
|
+
Date range
|
|
53
|
+
</legend>
|
|
54
|
+
<div className="flex flex-wrap gap-1.5" role="group" aria-label="Date range shortcuts">
|
|
55
|
+
{shortcut_options.map((option) => (
|
|
56
|
+
<button
|
|
57
|
+
key={option.value}
|
|
58
|
+
type="button"
|
|
59
|
+
className={get_button_class_name('ghost', 'small')}
|
|
60
|
+
onClick={() =>
|
|
61
|
+
on_range_change(
|
|
62
|
+
get_reporting_date_range_shortcut_inputs(
|
|
63
|
+
option.value,
|
|
64
|
+
new Date(),
|
|
65
|
+
week_starts_on_index,
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
>
|
|
70
|
+
{option.label}
|
|
71
|
+
</button>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
75
|
+
<label className="flex flex-col gap-1 text-[0.82rem] text-muted">
|
|
76
|
+
From
|
|
77
|
+
<input
|
|
78
|
+
type="date"
|
|
79
|
+
className={get_input_class_name('compact')}
|
|
80
|
+
value={range.from_date}
|
|
81
|
+
onChange={(event) =>
|
|
82
|
+
on_range_change({
|
|
83
|
+
...range,
|
|
84
|
+
from_date: event.target.value,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
/>
|
|
88
|
+
</label>
|
|
89
|
+
<label className="flex flex-col gap-1 text-[0.82rem] text-muted">
|
|
90
|
+
To
|
|
91
|
+
<input
|
|
92
|
+
type="date"
|
|
93
|
+
className={get_input_class_name('compact')}
|
|
94
|
+
value={range.to_date}
|
|
95
|
+
onChange={(event) =>
|
|
96
|
+
on_range_change({
|
|
97
|
+
...range,
|
|
98
|
+
to_date: event.target.value,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
/>
|
|
102
|
+
</label>
|
|
103
|
+
</div>
|
|
104
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
className={get_button_class_name('ghost', 'small')}
|
|
108
|
+
disabled={!has_filter}
|
|
109
|
+
onClick={on_clear}
|
|
110
|
+
>
|
|
111
|
+
Clear
|
|
112
|
+
</button>
|
|
113
|
+
{is_invalid ? (
|
|
114
|
+
<span className="text-[0.82rem] text-danger">
|
|
115
|
+
Enter a valid from and to date.
|
|
116
|
+
</span>
|
|
117
|
+
) : null}
|
|
118
|
+
</div>
|
|
119
|
+
</fieldset>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { get_button_class_name } from '@/lib/get_button_class_name'
|
|
4
|
+
import { type SheetReportSort } from '@/lib/types/reporting'
|
|
5
|
+
|
|
6
|
+
interface ReportingSortControlsProps {
|
|
7
|
+
sort: SheetReportSort
|
|
8
|
+
on_sort_change: (sort: SheetReportSort) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const sort_options: { value: SheetReportSort; label: string }[] = [
|
|
12
|
+
{ value: 'duration', label: 'Duration' },
|
|
13
|
+
{ value: 'name', label: 'Name' },
|
|
14
|
+
{ value: 'entry_count', label: 'Entries' },
|
|
15
|
+
{ value: 'active_first', label: 'Active first' },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sort mode toggles for the reporting sheet lists.
|
|
20
|
+
*/
|
|
21
|
+
export function ReportingSortControls({
|
|
22
|
+
sort,
|
|
23
|
+
on_sort_change,
|
|
24
|
+
}: ReportingSortControlsProps) {
|
|
25
|
+
return (
|
|
26
|
+
<fieldset className="m-0 flex w-full max-w-2xl flex-col gap-2 border-0 p-0">
|
|
27
|
+
<legend className="mb-2 text-[0.72rem] font-semibold uppercase tracking-[0.06em] text-muted">
|
|
28
|
+
Sort by
|
|
29
|
+
</legend>
|
|
30
|
+
<div className="flex flex-wrap gap-1.5" role="group" aria-label="Sort sheets by">
|
|
31
|
+
{sort_options.map((option) => {
|
|
32
|
+
const is_selected = sort === option.value
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<button
|
|
36
|
+
key={option.value}
|
|
37
|
+
type="button"
|
|
38
|
+
className={`${get_button_class_name('ghost', 'small')} ${
|
|
39
|
+
is_selected
|
|
40
|
+
? 'border-accent-border bg-accent-soft text-foreground'
|
|
41
|
+
: ''
|
|
42
|
+
}`}
|
|
43
|
+
aria-pressed={is_selected}
|
|
44
|
+
onClick={() => on_sort_change(option.value)}
|
|
45
|
+
>
|
|
46
|
+
{option.label}
|
|
47
|
+
</button>
|
|
48
|
+
)
|
|
49
|
+
})}
|
|
50
|
+
</div>
|
|
51
|
+
</fieldset>
|
|
52
|
+
)
|
|
53
|
+
}
|