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,196 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type FormEvent, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { use_confirm_dialog } from '@/components/confirm-dialog-provider'
|
|
6
|
+
import { SheetActionsMenu } from '@/components/sheet-actions-menu'
|
|
7
|
+
import { get_delete_sheet_confirm_dialog } from '@/lib/get_delete_sheet_confirm_dialog'
|
|
8
|
+
import { use_confirm_destructive_actions } from '@/lib/use_confirm_destructive_actions'
|
|
9
|
+
import { get_button_class_name } from '@/lib/get_button_class_name'
|
|
10
|
+
import { get_input_class_name } from '@/lib/get_input_class_name'
|
|
11
|
+
import { type SerializedSheet } from '@/lib/types/tracker_state'
|
|
12
|
+
|
|
13
|
+
interface SheetSidebarProps {
|
|
14
|
+
sheets: SerializedSheet[]
|
|
15
|
+
db_path: string
|
|
16
|
+
on_select: (name: string) => void
|
|
17
|
+
on_create: (name: string) => void
|
|
18
|
+
on_rename: (name: string, new_name: string) => void
|
|
19
|
+
on_delete: (name: string) => void
|
|
20
|
+
is_pending: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Lists sheets and supports switching, renaming, or creating new ones.
|
|
25
|
+
*/
|
|
26
|
+
export function SheetSidebar({
|
|
27
|
+
sheets,
|
|
28
|
+
db_path,
|
|
29
|
+
on_select,
|
|
30
|
+
on_create,
|
|
31
|
+
on_rename,
|
|
32
|
+
on_delete,
|
|
33
|
+
is_pending,
|
|
34
|
+
}: SheetSidebarProps) {
|
|
35
|
+
const { confirm } = use_confirm_dialog()
|
|
36
|
+
const confirm_destructive_actions = use_confirm_destructive_actions()
|
|
37
|
+
const can_delete_sheet = sheets.length > 1
|
|
38
|
+
const [new_sheet_name, set_new_sheet_name] = useState('')
|
|
39
|
+
const [editing_sheet_name, set_editing_sheet_name] = useState<string | null>(
|
|
40
|
+
null,
|
|
41
|
+
)
|
|
42
|
+
const [edited_sheet_name, set_edited_sheet_name] = useState('')
|
|
43
|
+
|
|
44
|
+
const handle_create = (event: FormEvent<HTMLFormElement>): void => {
|
|
45
|
+
event.preventDefault()
|
|
46
|
+
const trimmed = new_sheet_name.trim()
|
|
47
|
+
|
|
48
|
+
if (trimmed.length === 0) {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
on_create(trimmed)
|
|
53
|
+
set_new_sheet_name('')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const start_rename = (sheet_name: string): void => {
|
|
57
|
+
set_editing_sheet_name(sheet_name)
|
|
58
|
+
set_edited_sheet_name(sheet_name)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const cancel_rename = (): void => {
|
|
62
|
+
set_editing_sheet_name(null)
|
|
63
|
+
set_edited_sheet_name('')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const handle_rename = (event: FormEvent<HTMLFormElement>): void => {
|
|
67
|
+
event.preventDefault()
|
|
68
|
+
|
|
69
|
+
if (editing_sheet_name === null) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const trimmed = edited_sheet_name.trim()
|
|
74
|
+
|
|
75
|
+
if (trimmed.length === 0 || trimmed === editing_sheet_name) {
|
|
76
|
+
cancel_rename()
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
on_rename(editing_sheet_name, trimmed)
|
|
81
|
+
cancel_rename()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<aside className="flex min-w-0 flex-col gap-3 max-[860px]:min-w-0">
|
|
86
|
+
<h2 className="m-0 text-[0.72rem] font-semibold uppercase tracking-[0.06em] text-muted">
|
|
87
|
+
Sheets
|
|
88
|
+
</h2>
|
|
89
|
+
<ul className="m-0 flex min-h-0 flex-1 list-none flex-col gap-1.5 p-0">
|
|
90
|
+
{sheets.map((sheet) => (
|
|
91
|
+
<li key={sheet.name} className="min-w-0">
|
|
92
|
+
{editing_sheet_name === sheet.name ? (
|
|
93
|
+
<form className="flex w-full min-w-0 flex-col gap-1.5" onSubmit={handle_rename}>
|
|
94
|
+
<input
|
|
95
|
+
className={get_input_class_name('compact')}
|
|
96
|
+
value={edited_sheet_name}
|
|
97
|
+
onChange={(event) => set_edited_sheet_name(event.target.value)}
|
|
98
|
+
disabled={is_pending}
|
|
99
|
+
autoFocus
|
|
100
|
+
/>
|
|
101
|
+
<div className="flex gap-1.5">
|
|
102
|
+
<button
|
|
103
|
+
type="submit"
|
|
104
|
+
className={`${get_button_class_name('ghost')} flex-1`}
|
|
105
|
+
disabled={
|
|
106
|
+
is_pending || edited_sheet_name.trim().length === 0
|
|
107
|
+
}
|
|
108
|
+
>
|
|
109
|
+
Save
|
|
110
|
+
</button>
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
className={`${get_button_class_name('ghost')} flex-1`}
|
|
114
|
+
disabled={is_pending}
|
|
115
|
+
onClick={cancel_rename}
|
|
116
|
+
>
|
|
117
|
+
Cancel
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
</form>
|
|
121
|
+
) : (
|
|
122
|
+
<div className="flex min-w-0 items-stretch gap-1">
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
className="flex min-w-0 flex-1 cursor-pointer items-center justify-between gap-2 rounded-md border border-transparent bg-transparent px-2.5 py-2 text-left transition-[background-color,color] duration-150 hover:bg-surface-hover"
|
|
126
|
+
disabled={is_pending}
|
|
127
|
+
onClick={() => on_select(sheet.name)}
|
|
128
|
+
>
|
|
129
|
+
<span
|
|
130
|
+
className={`min-w-0 flex-1 truncate font-semibold ${
|
|
131
|
+
sheet.isActive || sheet.hasActiveEntry ? 'text-accent' : ''
|
|
132
|
+
}`}
|
|
133
|
+
>
|
|
134
|
+
{sheet.name}
|
|
135
|
+
</span>
|
|
136
|
+
<span
|
|
137
|
+
className={`shrink-0 text-xs whitespace-nowrap ${
|
|
138
|
+
sheet.hasActiveEntry ? 'text-accent' : 'text-muted'
|
|
139
|
+
}`}
|
|
140
|
+
>
|
|
141
|
+
{sheet.hasActiveEntry
|
|
142
|
+
? '● active'
|
|
143
|
+
: `${sheet.entryCount} entries`}
|
|
144
|
+
</span>
|
|
145
|
+
</button>
|
|
146
|
+
<SheetActionsMenu
|
|
147
|
+
sheet_name={sheet.name}
|
|
148
|
+
is_pending={is_pending}
|
|
149
|
+
can_delete={can_delete_sheet}
|
|
150
|
+
on_rename={() => start_rename(sheet.name)}
|
|
151
|
+
on_delete={async () => {
|
|
152
|
+
const confirmed = confirm_destructive_actions
|
|
153
|
+
? await confirm(
|
|
154
|
+
get_delete_sheet_confirm_dialog(
|
|
155
|
+
sheet.name,
|
|
156
|
+
sheet.entryCount,
|
|
157
|
+
sheet.hasActiveEntry,
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
: true
|
|
161
|
+
|
|
162
|
+
if (confirmed) {
|
|
163
|
+
on_delete(sheet.name)
|
|
164
|
+
}
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
</li>
|
|
170
|
+
))}
|
|
171
|
+
</ul>
|
|
172
|
+
<form className="flex w-full min-w-0 flex-col gap-2" onSubmit={handle_create}>
|
|
173
|
+
<input
|
|
174
|
+
className={get_input_class_name('compact')}
|
|
175
|
+
value={new_sheet_name}
|
|
176
|
+
onChange={(event) => set_new_sheet_name(event.target.value)}
|
|
177
|
+
placeholder="New sheet name"
|
|
178
|
+
disabled={is_pending}
|
|
179
|
+
/>
|
|
180
|
+
<button
|
|
181
|
+
type="submit"
|
|
182
|
+
className={get_button_class_name('ghost')}
|
|
183
|
+
disabled={is_pending || new_sheet_name.trim().length === 0}
|
|
184
|
+
>
|
|
185
|
+
Add
|
|
186
|
+
</button>
|
|
187
|
+
</form>
|
|
188
|
+
<p
|
|
189
|
+
className="mt-auto shrink-0 overflow-wrap-anywhere border-t border-panel-border pt-3 font-mono text-[0.65rem] leading-snug text-muted"
|
|
190
|
+
title={db_path}
|
|
191
|
+
>
|
|
192
|
+
{db_path}
|
|
193
|
+
</p>
|
|
194
|
+
</aside>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type KeyboardEvent,
|
|
5
|
+
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react'
|
|
10
|
+
|
|
11
|
+
import { apply_tag_autocomplete_selection } from '@/lib/apply_tag_autocomplete_selection'
|
|
12
|
+
import { filter_known_tags } from '@/lib/filter_known_tags'
|
|
13
|
+
import { format_display_tag } from '@/lib/format_display_tag'
|
|
14
|
+
import { get_tag_autocomplete_context } from '@/lib/get_tag_autocomplete_context'
|
|
15
|
+
import { get_input_class_name } from '@/lib/get_input_class_name'
|
|
16
|
+
|
|
17
|
+
interface TagAutocompleteInputProps {
|
|
18
|
+
id: string
|
|
19
|
+
value: string
|
|
20
|
+
known_tags: string[]
|
|
21
|
+
placeholder?: string
|
|
22
|
+
disabled?: boolean
|
|
23
|
+
autoFocus?: boolean
|
|
24
|
+
on_change: (value: string) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const option_class =
|
|
28
|
+
'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'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Text input that suggests existing @tags while typing.
|
|
32
|
+
*/
|
|
33
|
+
export function TagAutocompleteInput({
|
|
34
|
+
id,
|
|
35
|
+
value,
|
|
36
|
+
known_tags,
|
|
37
|
+
placeholder,
|
|
38
|
+
disabled = false,
|
|
39
|
+
autoFocus = false,
|
|
40
|
+
on_change,
|
|
41
|
+
}: TagAutocompleteInputProps) {
|
|
42
|
+
const input_ref = useRef<HTMLInputElement>(null)
|
|
43
|
+
const list_ref = useRef<HTMLUListElement>(null)
|
|
44
|
+
const pending_cursor_ref = useRef<number | null>(null)
|
|
45
|
+
const [cursor_index, set_cursor_index] = useState(0)
|
|
46
|
+
const [highlighted_index, set_highlighted_index] = useState(0)
|
|
47
|
+
|
|
48
|
+
const context = get_tag_autocomplete_context(value, cursor_index)
|
|
49
|
+
const suggestions =
|
|
50
|
+
context === null ? [] : filter_known_tags(known_tags, context.query)
|
|
51
|
+
const is_open = context !== null && suggestions.length > 0 && !disabled
|
|
52
|
+
|
|
53
|
+
useLayoutEffect(() => {
|
|
54
|
+
if (pending_cursor_ref.current === null || input_ref.current === null) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
input_ref.current.setSelectionRange(
|
|
59
|
+
pending_cursor_ref.current,
|
|
60
|
+
pending_cursor_ref.current,
|
|
61
|
+
)
|
|
62
|
+
set_cursor_index(pending_cursor_ref.current)
|
|
63
|
+
pending_cursor_ref.current = null
|
|
64
|
+
}, [value])
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
set_highlighted_index(0)
|
|
68
|
+
}, [value, cursor_index, suggestions.length])
|
|
69
|
+
|
|
70
|
+
const update_cursor_from_input = (): void => {
|
|
71
|
+
set_cursor_index(input_ref.current?.selectionStart ?? value.length)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const close_autocomplete = (): void => {
|
|
75
|
+
set_highlighted_index(0)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const apply_tag = (tag: string): void => {
|
|
79
|
+
if (context === null) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { next_text, next_cursor } = apply_tag_autocomplete_selection(
|
|
84
|
+
value,
|
|
85
|
+
context,
|
|
86
|
+
tag,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
pending_cursor_ref.current = next_cursor
|
|
90
|
+
on_change(next_text)
|
|
91
|
+
close_autocomplete()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const handle_key_down = (event: KeyboardEvent<HTMLInputElement>): void => {
|
|
95
|
+
if (!is_open) {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (event.key === 'ArrowDown') {
|
|
100
|
+
event.preventDefault()
|
|
101
|
+
set_highlighted_index((index) => (index + 1) % suggestions.length)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (event.key === 'ArrowUp') {
|
|
106
|
+
event.preventDefault()
|
|
107
|
+
set_highlighted_index(
|
|
108
|
+
(index) => (index - 1 + suggestions.length) % suggestions.length,
|
|
109
|
+
)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (event.key === 'Enter' || event.key === 'Tab') {
|
|
114
|
+
event.preventDefault()
|
|
115
|
+
const selected = suggestions[highlighted_index]
|
|
116
|
+
|
|
117
|
+
if (selected !== undefined) {
|
|
118
|
+
apply_tag(selected)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (event.key === 'Escape') {
|
|
125
|
+
event.preventDefault()
|
|
126
|
+
close_autocomplete()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className="relative min-w-0">
|
|
132
|
+
<input
|
|
133
|
+
ref={input_ref}
|
|
134
|
+
id={id}
|
|
135
|
+
type="text"
|
|
136
|
+
className={get_input_class_name()}
|
|
137
|
+
value={value}
|
|
138
|
+
placeholder={placeholder}
|
|
139
|
+
disabled={disabled}
|
|
140
|
+
autoFocus={autoFocus}
|
|
141
|
+
autoComplete="off"
|
|
142
|
+
aria-autocomplete="list"
|
|
143
|
+
aria-expanded={is_open}
|
|
144
|
+
aria-controls={is_open ? `${id}-tag-suggestions` : undefined}
|
|
145
|
+
onChange={(event) => {
|
|
146
|
+
on_change(event.target.value)
|
|
147
|
+
set_cursor_index(event.target.selectionStart ?? event.target.value.length)
|
|
148
|
+
}}
|
|
149
|
+
onClick={update_cursor_from_input}
|
|
150
|
+
onKeyUp={update_cursor_from_input}
|
|
151
|
+
onKeyDown={handle_key_down}
|
|
152
|
+
onBlur={() => {
|
|
153
|
+
window.setTimeout(close_autocomplete, 120)
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
{is_open ? (
|
|
157
|
+
<ul
|
|
158
|
+
ref={list_ref}
|
|
159
|
+
id={`${id}-tag-suggestions`}
|
|
160
|
+
role="listbox"
|
|
161
|
+
className="absolute left-0 right-0 top-full z-20 mt-1 max-h-48 list-none overflow-y-auto rounded-md border border-panel-border bg-panel p-1.5 shadow-md"
|
|
162
|
+
>
|
|
163
|
+
{suggestions.map((tag, index) => (
|
|
164
|
+
<li key={tag} role="option" aria-selected={index === highlighted_index}>
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
className={`${option_class} ${
|
|
168
|
+
index === highlighted_index ? 'bg-surface-hover' : ''
|
|
169
|
+
}`}
|
|
170
|
+
onMouseDown={(event) => {
|
|
171
|
+
event.preventDefault()
|
|
172
|
+
apply_tag(tag)
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
{format_display_tag(tag)}
|
|
176
|
+
</button>
|
|
177
|
+
</li>
|
|
178
|
+
))}
|
|
179
|
+
</ul>
|
|
180
|
+
) : null}
|
|
181
|
+
</div>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore } from 'react'
|
|
4
|
+
|
|
5
|
+
import { SettingRadioGroup } from '@/components/setting-radio-group'
|
|
6
|
+
import { tag_filter_mode_preference } from '@/lib/preferences/tag_filter_mode_preference'
|
|
7
|
+
import { persist_ui_preference } from '@/lib/persist_ui_preference'
|
|
8
|
+
import { type TagFilterMode } from '@/lib/types/ui_preferences'
|
|
9
|
+
|
|
10
|
+
const options: { value: TagFilterMode; label: string; description: string }[] = [
|
|
11
|
+
{
|
|
12
|
+
value: 'all',
|
|
13
|
+
label: 'Match all tags',
|
|
14
|
+
description: 'Entry must include every selected tag.',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
value: 'any',
|
|
18
|
+
label: 'Match any tag',
|
|
19
|
+
description: 'Entry can include any one of the selected tags.',
|
|
20
|
+
},
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const set_tag_filter_mode = (value: TagFilterMode): void => {
|
|
24
|
+
persist_ui_preference(tag_filter_mode_preference, value)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Setting: how multiple tag filters are combined.
|
|
29
|
+
*/
|
|
30
|
+
export function TagFilterModeSetting() {
|
|
31
|
+
const value = useSyncExternalStore(
|
|
32
|
+
tag_filter_mode_preference.subscribe,
|
|
33
|
+
tag_filter_mode_preference.get_snapshot,
|
|
34
|
+
tag_filter_mode_preference.get_server_snapshot,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<SettingRadioGroup<TagFilterMode>
|
|
39
|
+
name="tag-filter-mode"
|
|
40
|
+
legend="Tag filter mode"
|
|
41
|
+
description="How entries match when multiple tags are selected on a sheet."
|
|
42
|
+
value={value}
|
|
43
|
+
options={options}
|
|
44
|
+
on_change={set_tag_filter_mode}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
}
|