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,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
|
+
}
|