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,75 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
|
|
3
|
+
import { api_error_response } from '@/lib/api_error_response'
|
|
4
|
+
import { collect_tag_stats } from '@/lib/collect_tag_stats'
|
|
5
|
+
import { merge_tags_across_db } from '@/lib/merge_tags_across_db'
|
|
6
|
+
import { read_db } from '@/lib/read_db'
|
|
7
|
+
import { rename_tag_across_db } from '@/lib/rename_tag_across_db'
|
|
8
|
+
|
|
9
|
+
interface RenameTagBody {
|
|
10
|
+
action: 'rename'
|
|
11
|
+
fromTag?: string
|
|
12
|
+
toTag?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface MergeTagsBody {
|
|
16
|
+
action: 'merge'
|
|
17
|
+
sourceTags?: string[]
|
|
18
|
+
targetTag?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type TagMutationBody = RenameTagBody | MergeTagsBody
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns tag usage statistics for the tag management screen.
|
|
25
|
+
*/
|
|
26
|
+
export async function GET(): Promise<NextResponse> {
|
|
27
|
+
try {
|
|
28
|
+
const db = await read_db()
|
|
29
|
+
|
|
30
|
+
return NextResponse.json({ tags: collect_tag_stats(db) })
|
|
31
|
+
} catch (error: unknown) {
|
|
32
|
+
return api_error_response(error, 500)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Renames or merges tags across all entries.
|
|
38
|
+
*/
|
|
39
|
+
export async function PATCH(request: Request): Promise<NextResponse> {
|
|
40
|
+
try {
|
|
41
|
+
const body = (await request.json()) as TagMutationBody
|
|
42
|
+
|
|
43
|
+
if (body.action === 'rename') {
|
|
44
|
+
const from_tag = body.fromTag?.trim() ?? ''
|
|
45
|
+
const to_tag = body.toTag?.trim() ?? ''
|
|
46
|
+
|
|
47
|
+
if (from_tag.length === 0 || to_tag.length === 0) {
|
|
48
|
+
return api_error_response(new Error('Both tag names are required.'))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = await rename_tag_across_db(from_tag, to_tag)
|
|
52
|
+
|
|
53
|
+
return NextResponse.json(result)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (body.action === 'merge') {
|
|
57
|
+
const source_tags = body.sourceTags ?? []
|
|
58
|
+
const target_tag = body.targetTag?.trim() ?? ''
|
|
59
|
+
|
|
60
|
+
if (source_tags.length === 0 || target_tag.length === 0) {
|
|
61
|
+
return api_error_response(
|
|
62
|
+
new Error('Source tags and a target tag are required.'),
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const result = await merge_tags_across_db(source_tags, target_tag)
|
|
67
|
+
|
|
68
|
+
return NextResponse.json(result)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return api_error_response(new Error('Unknown tag action.'))
|
|
72
|
+
} catch (error: unknown) {
|
|
73
|
+
return api_error_response(error, 400)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/* Midnight — cool, deep */
|
|
2
|
+
[data-theme='dark'][data-palette='midnight'] {
|
|
3
|
+
--background: #05070b;
|
|
4
|
+
--foreground: #e4eaf4;
|
|
5
|
+
--panel: #0c1018;
|
|
6
|
+
--panel-border: #1e2838;
|
|
7
|
+
--surface-raised: #111722;
|
|
8
|
+
--surface-hover: #182030;
|
|
9
|
+
--muted: #8a97ad;
|
|
10
|
+
--input-bg: #080c12;
|
|
11
|
+
--ghost-bg: #151d2a;
|
|
12
|
+
--tag-bg: #1a2434;
|
|
13
|
+
--tag-text: #b4c2d8;
|
|
14
|
+
--overlay: #0000008c;
|
|
15
|
+
--accent: #60a5fa;
|
|
16
|
+
--accent-soft: #0e2240;
|
|
17
|
+
--accent-border: #60a5fa55;
|
|
18
|
+
--accent-text-on: #0b1a32;
|
|
19
|
+
--input-focus-border: #60a5fa88;
|
|
20
|
+
--active-panel-bg: linear-gradient(160deg, #0e2240 0%, #0c1018 55%);
|
|
21
|
+
--body-gradient: radial-gradient(
|
|
22
|
+
ellipse 80% 50% at 50% -10%,
|
|
23
|
+
#152238 0%,
|
|
24
|
+
transparent 70%
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
[data-theme='light'][data-palette='midnight'] {
|
|
29
|
+
--background: #e4e9f2;
|
|
30
|
+
--foreground: #0c1424;
|
|
31
|
+
--panel: #f8fafc;
|
|
32
|
+
--panel-border: #c5d0e2;
|
|
33
|
+
--surface-raised: #eef2f8;
|
|
34
|
+
--surface-hover: #e2e8f2;
|
|
35
|
+
--muted: #4f5f78;
|
|
36
|
+
--input-bg: #ffffff;
|
|
37
|
+
--ghost-bg: #e8edf5;
|
|
38
|
+
--tag-bg: #dce4f0;
|
|
39
|
+
--tag-text: #2a3a52;
|
|
40
|
+
--overlay: #0c142466;
|
|
41
|
+
--accent: #2563eb;
|
|
42
|
+
--accent-soft: #eff6ff;
|
|
43
|
+
--accent-border: #93c5fd55;
|
|
44
|
+
--accent-text-on: #ffffff;
|
|
45
|
+
--input-focus-border: #2563eb88;
|
|
46
|
+
--active-panel-bg: linear-gradient(160deg, #eff6ff 0%, #f8fafc 50%);
|
|
47
|
+
--body-gradient: radial-gradient(
|
|
48
|
+
ellipse 80% 50% at 50% -10%,
|
|
49
|
+
#c8d8f0 0%,
|
|
50
|
+
transparent 70%
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Warm — stone / sepia */
|
|
55
|
+
[data-theme='dark'][data-palette='warm'] {
|
|
56
|
+
--background: #100e0c;
|
|
57
|
+
--foreground: #f0ebe3;
|
|
58
|
+
--panel: #1a1714;
|
|
59
|
+
--panel-border: #322c26;
|
|
60
|
+
--surface-raised: #211e1a;
|
|
61
|
+
--surface-hover: #2c2722;
|
|
62
|
+
--muted: #a89a8c;
|
|
63
|
+
--input-bg: #141210;
|
|
64
|
+
--ghost-bg: #252019;
|
|
65
|
+
--tag-bg: #2e2820;
|
|
66
|
+
--tag-text: #d4c8b8;
|
|
67
|
+
--overlay: #00000080;
|
|
68
|
+
--accent: #fbbf24;
|
|
69
|
+
--accent-soft: #3a2a0a;
|
|
70
|
+
--accent-border: #fbbf2455;
|
|
71
|
+
--accent-text-on: #422006;
|
|
72
|
+
--input-focus-border: #fbbf2488;
|
|
73
|
+
--active-panel-bg: linear-gradient(160deg, #3a2a0a 0%, #1a1714 55%);
|
|
74
|
+
--body-gradient: radial-gradient(
|
|
75
|
+
ellipse 80% 50% at 50% -10%,
|
|
76
|
+
#3a2a18 0%,
|
|
77
|
+
transparent 70%
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
[data-theme='light'][data-palette='warm'] {
|
|
82
|
+
--background: #f3ede4;
|
|
83
|
+
--foreground: #1c1814;
|
|
84
|
+
--panel: #fffdf9;
|
|
85
|
+
--panel-border: #ddd4c8;
|
|
86
|
+
--surface-raised: #f8f4ec;
|
|
87
|
+
--surface-hover: #efe8dc;
|
|
88
|
+
--muted: #6b5f52;
|
|
89
|
+
--input-bg: #ffffff;
|
|
90
|
+
--ghost-bg: #f0e8dc;
|
|
91
|
+
--tag-bg: #e8dfd0;
|
|
92
|
+
--tag-text: #4a4034;
|
|
93
|
+
--overlay: #1c181466;
|
|
94
|
+
--accent: #b45309;
|
|
95
|
+
--accent-soft: #fffbeb;
|
|
96
|
+
--accent-border: #fcd34d55;
|
|
97
|
+
--accent-text-on: #ffffff;
|
|
98
|
+
--input-focus-border: #b4530988;
|
|
99
|
+
--active-panel-bg: linear-gradient(160deg, #fffbeb 0%, #fffdf9 50%);
|
|
100
|
+
--body-gradient: radial-gradient(
|
|
101
|
+
ellipse 80% 50% at 50% -10%,
|
|
102
|
+
#ead9c0 0%,
|
|
103
|
+
transparent 70%
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Ocean — blue-tinted */
|
|
108
|
+
[data-theme='dark'][data-palette='ocean'] {
|
|
109
|
+
--background: #061018;
|
|
110
|
+
--foreground: #e2edf5;
|
|
111
|
+
--panel: #0c1620;
|
|
112
|
+
--panel-border: #1a2a3c;
|
|
113
|
+
--surface-raised: #101c28;
|
|
114
|
+
--surface-hover: #162636;
|
|
115
|
+
--muted: #88a0b4;
|
|
116
|
+
--input-bg: #081018;
|
|
117
|
+
--ghost-bg: #142030;
|
|
118
|
+
--tag-bg: #1a2c40;
|
|
119
|
+
--tag-text: #b8cce0;
|
|
120
|
+
--overlay: #00000085;
|
|
121
|
+
--accent: #38bdf8;
|
|
122
|
+
--accent-soft: #0c2a40;
|
|
123
|
+
--accent-border: #38bdf855;
|
|
124
|
+
--accent-text-on: #082032;
|
|
125
|
+
--input-focus-border: #38bdf888;
|
|
126
|
+
--active-panel-bg: linear-gradient(160deg, #0c2a40 0%, #0c1620 55%);
|
|
127
|
+
--body-gradient: radial-gradient(
|
|
128
|
+
ellipse 80% 50% at 50% -10%,
|
|
129
|
+
#123050 0%,
|
|
130
|
+
transparent 70%
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
[data-theme='light'][data-palette='ocean'] {
|
|
135
|
+
--background: #e2eff8;
|
|
136
|
+
--foreground: #0a1828;
|
|
137
|
+
--panel: #f8fcff;
|
|
138
|
+
--panel-border: #c4d8ea;
|
|
139
|
+
--surface-raised: #eef6fc;
|
|
140
|
+
--surface-hover: #e0eef8;
|
|
141
|
+
--muted: #4a6478;
|
|
142
|
+
--input-bg: #ffffff;
|
|
143
|
+
--ghost-bg: #e4f0fa;
|
|
144
|
+
--tag-bg: #d4e8f4;
|
|
145
|
+
--tag-text: #2a4860;
|
|
146
|
+
--overlay: #0a182866;
|
|
147
|
+
--accent: #0284c7;
|
|
148
|
+
--accent-soft: #e0f2fe;
|
|
149
|
+
--accent-border: #7dd3fc55;
|
|
150
|
+
--accent-text-on: #ffffff;
|
|
151
|
+
--input-focus-border: #0284c788;
|
|
152
|
+
--active-panel-bg: linear-gradient(160deg, #e0f2fe 0%, #f8fcff 50%);
|
|
153
|
+
--body-gradient: radial-gradient(
|
|
154
|
+
ellipse 80% 50% at 50% -10%,
|
|
155
|
+
#b8daf0 0%,
|
|
156
|
+
transparent 70%
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* Forest — green-gray */
|
|
161
|
+
[data-theme='dark'][data-palette='forest'] {
|
|
162
|
+
--background: #080e0a;
|
|
163
|
+
--foreground: #e4ede6;
|
|
164
|
+
--panel: #101814;
|
|
165
|
+
--panel-border: #1e2e24;
|
|
166
|
+
--surface-raised: #141e18;
|
|
167
|
+
--surface-hover: #1a2a20;
|
|
168
|
+
--muted: #8aa494;
|
|
169
|
+
--input-bg: #0a100c;
|
|
170
|
+
--ghost-bg: #162018;
|
|
171
|
+
--tag-bg: #1c2e22;
|
|
172
|
+
--tag-text: #b8d0be;
|
|
173
|
+
--overlay: #00000080;
|
|
174
|
+
--accent: #34d399;
|
|
175
|
+
--accent-soft: #0d2f25;
|
|
176
|
+
--accent-border: #34d39955;
|
|
177
|
+
--accent-text-on: #022c22;
|
|
178
|
+
--input-focus-border: #34d39988;
|
|
179
|
+
--active-panel-bg: linear-gradient(160deg, #0d2f25 0%, #101814 55%);
|
|
180
|
+
--body-gradient: radial-gradient(
|
|
181
|
+
ellipse 80% 50% at 50% -10%,
|
|
182
|
+
#143020 0%,
|
|
183
|
+
transparent 70%
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
[data-theme='light'][data-palette='forest'] {
|
|
188
|
+
--background: #eaf2ec;
|
|
189
|
+
--foreground: #0e1a12;
|
|
190
|
+
--panel: #f9fbf9;
|
|
191
|
+
--panel-border: #c8dcd0;
|
|
192
|
+
--surface-raised: #f0f6f2;
|
|
193
|
+
--surface-hover: #e4eee6;
|
|
194
|
+
--muted: #4a6454;
|
|
195
|
+
--input-bg: #ffffff;
|
|
196
|
+
--ghost-bg: #e6f0e8;
|
|
197
|
+
--tag-bg: #d8e8dc;
|
|
198
|
+
--tag-text: #2a4434;
|
|
199
|
+
--overlay: #0e1a1266;
|
|
200
|
+
--accent: #047857;
|
|
201
|
+
--accent-soft: #ecfdf5;
|
|
202
|
+
--accent-border: #6ee7b755;
|
|
203
|
+
--accent-text-on: #ffffff;
|
|
204
|
+
--input-focus-border: #04785788;
|
|
205
|
+
--active-panel-bg: linear-gradient(160deg, #ecfdf5 0%, #f9fbf9 50%);
|
|
206
|
+
--body-gradient: radial-gradient(
|
|
207
|
+
ellipse 80% 50% at 50% -10%,
|
|
208
|
+
#c4e0cc 0%,
|
|
209
|
+
transparent 70%
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* High contrast */
|
|
214
|
+
[data-theme='dark'][data-palette='contrast'] {
|
|
215
|
+
--background: #000000;
|
|
216
|
+
--foreground: #ffffff;
|
|
217
|
+
--panel: #121212;
|
|
218
|
+
--panel-border: #5a5a5a;
|
|
219
|
+
--surface-raised: #1a1a1a;
|
|
220
|
+
--surface-hover: #262626;
|
|
221
|
+
--muted: #c8c8c8;
|
|
222
|
+
--input-bg: #0a0a0a;
|
|
223
|
+
--ghost-bg: #222222;
|
|
224
|
+
--tag-bg: #2e2e2e;
|
|
225
|
+
--tag-text: #f0f0f0;
|
|
226
|
+
--overlay: #000000b3;
|
|
227
|
+
--accent: #ffffff;
|
|
228
|
+
--accent-soft: #333333;
|
|
229
|
+
--accent-border: #ffffff66;
|
|
230
|
+
--accent-text-on: #000000;
|
|
231
|
+
--input-focus-border: #ffffff88;
|
|
232
|
+
--active-panel-bg: linear-gradient(160deg, #1a1a1a 0%, #121212 55%);
|
|
233
|
+
--shadow-sm: 0 1px 2px #00000080;
|
|
234
|
+
--shadow-md: 0 8px 24px #000000a0;
|
|
235
|
+
--body-gradient: none;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
[data-theme='light'][data-palette='contrast'] {
|
|
239
|
+
--background: #ffffff;
|
|
240
|
+
--foreground: #000000;
|
|
241
|
+
--panel: #f5f5f5;
|
|
242
|
+
--panel-border: #404040;
|
|
243
|
+
--surface-raised: #eeeeee;
|
|
244
|
+
--surface-hover: #e0e0e0;
|
|
245
|
+
--muted: #333333;
|
|
246
|
+
--input-bg: #ffffff;
|
|
247
|
+
--ghost-bg: #e8e8e8;
|
|
248
|
+
--tag-bg: #d8d8d8;
|
|
249
|
+
--tag-text: #111111;
|
|
250
|
+
--overlay: #00000040;
|
|
251
|
+
--accent: #000000;
|
|
252
|
+
--accent-soft: #e0e0e0;
|
|
253
|
+
--accent-border: #00000044;
|
|
254
|
+
--accent-text-on: #ffffff;
|
|
255
|
+
--input-focus-border: #00000088;
|
|
256
|
+
--active-panel-bg: linear-gradient(160deg, #eeeeee 0%, #f5f5f5 50%);
|
|
257
|
+
--shadow-sm: 0 1px 2px #00000020;
|
|
258
|
+
--shadow-md: 0 8px 24px #00000030;
|
|
259
|
+
--body-gradient: none;
|
|
260
|
+
}
|
package/app/favicon.ico
ADDED
|
Binary file
|
package/app/globals.css
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
@import './color-palettes.css';
|
|
3
|
+
|
|
4
|
+
@custom-variant compact (&:is([data-compact-lists='true'], [data-compact-lists='true'] *));
|
|
5
|
+
|
|
6
|
+
[data-theme='dark'] {
|
|
7
|
+
color-scheme: dark;
|
|
8
|
+
--background: #090b10;
|
|
9
|
+
--foreground: #e8edf5;
|
|
10
|
+
--panel: #12161f;
|
|
11
|
+
--panel-border: #252f42;
|
|
12
|
+
--surface-raised: #171d28;
|
|
13
|
+
--surface-hover: #1e2634;
|
|
14
|
+
--muted: #8b98ad;
|
|
15
|
+
--accent: #5eead4;
|
|
16
|
+
--accent-soft: #0f2e2b;
|
|
17
|
+
--accent-border: #2dd4bf55;
|
|
18
|
+
--accent-text-on: #042f2e;
|
|
19
|
+
--danger: #fb7185;
|
|
20
|
+
--danger-soft: #3a1520;
|
|
21
|
+
--danger-border: #fb718544;
|
|
22
|
+
--danger-text: #fecdd3;
|
|
23
|
+
--input-bg: #0d1118;
|
|
24
|
+
--ghost-bg: #1a2230;
|
|
25
|
+
--tag-bg: #1f2a3d;
|
|
26
|
+
--tag-text: #b9c7de;
|
|
27
|
+
--overlay: #00000080;
|
|
28
|
+
--active-panel-bg: linear-gradient(160deg, #0f2420 0%, #12161f 55%);
|
|
29
|
+
--body-gradient: radial-gradient(ellipse 80% 50% at 50% -10%, #1a2a40 0%, transparent 70%);
|
|
30
|
+
--input-focus-border: #2dd4bf88;
|
|
31
|
+
--shadow-sm: 0 1px 2px #00000040;
|
|
32
|
+
--shadow-md: 0 8px 24px #00000055;
|
|
33
|
+
--radius-sm: 0.5rem;
|
|
34
|
+
--radius-md: 0.75rem;
|
|
35
|
+
--radius-lg: 1rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
[data-theme='light'] {
|
|
39
|
+
color-scheme: light;
|
|
40
|
+
--background: #eef1f6;
|
|
41
|
+
--foreground: #0f1729;
|
|
42
|
+
--panel: #ffffff;
|
|
43
|
+
--panel-border: #d8e0ec;
|
|
44
|
+
--surface-raised: #f8fafc;
|
|
45
|
+
--surface-hover: #eef2f7;
|
|
46
|
+
--muted: #5c6b82;
|
|
47
|
+
--accent: #0d9488;
|
|
48
|
+
--accent-soft: #ecfdf5;
|
|
49
|
+
--accent-border: #99f6e455;
|
|
50
|
+
--accent-text-on: #ffffff;
|
|
51
|
+
--danger: #e11d48;
|
|
52
|
+
--danger-soft: #fff1f2;
|
|
53
|
+
--danger-border: #fecdd3;
|
|
54
|
+
--danger-text: #9f1239;
|
|
55
|
+
--input-bg: #ffffff;
|
|
56
|
+
--ghost-bg: #f1f5f9;
|
|
57
|
+
--tag-bg: #e8eef6;
|
|
58
|
+
--tag-text: #3d4f66;
|
|
59
|
+
--overlay: #0f172966;
|
|
60
|
+
--active-panel-bg: linear-gradient(160deg, #ecfdf5 0%, #ffffff 50%);
|
|
61
|
+
--body-gradient: radial-gradient(ellipse 80% 50% at 50% -10%, #dbeafe 0%, transparent 70%);
|
|
62
|
+
--input-focus-border: #0d948888;
|
|
63
|
+
--shadow-sm: 0 1px 2px #0f172912;
|
|
64
|
+
--shadow-md: 0 10px 30px #0f172918;
|
|
65
|
+
--radius-sm: 0.5rem;
|
|
66
|
+
--radius-md: 0.75rem;
|
|
67
|
+
--radius-lg: 1rem;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
html[data-theme='dark'],
|
|
71
|
+
html[data-theme='light'] {
|
|
72
|
+
min-height: 100%;
|
|
73
|
+
background-color: var(--background);
|
|
74
|
+
background-image: var(--body-gradient);
|
|
75
|
+
background-repeat: no-repeat;
|
|
76
|
+
background-attachment: fixed;
|
|
77
|
+
color: var(--foreground);
|
|
78
|
+
accent-color: var(--accent);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
body {
|
|
82
|
+
min-height: 100%;
|
|
83
|
+
background-color: transparent;
|
|
84
|
+
color: inherit;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
a {
|
|
88
|
+
color: var(--accent);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
:focus-visible {
|
|
92
|
+
outline: 2px solid var(--accent);
|
|
93
|
+
outline-offset: 2px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
::selection {
|
|
97
|
+
background: var(--accent-soft);
|
|
98
|
+
color: var(--foreground);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
input::placeholder,
|
|
102
|
+
textarea::placeholder {
|
|
103
|
+
color: var(--muted);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
select option {
|
|
107
|
+
background: var(--panel);
|
|
108
|
+
color: var(--foreground);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@theme inline {
|
|
112
|
+
--color-background: var(--background);
|
|
113
|
+
--color-foreground: var(--foreground);
|
|
114
|
+
--color-panel: var(--panel);
|
|
115
|
+
--color-panel-border: var(--panel-border);
|
|
116
|
+
--color-surface-raised: var(--surface-raised);
|
|
117
|
+
--color-surface-hover: var(--surface-hover);
|
|
118
|
+
--color-muted: var(--muted);
|
|
119
|
+
--color-accent: var(--accent);
|
|
120
|
+
--color-accent-soft: var(--accent-soft);
|
|
121
|
+
--color-accent-border: var(--accent-border);
|
|
122
|
+
--color-accent-text-on: var(--accent-text-on);
|
|
123
|
+
--color-danger: var(--danger);
|
|
124
|
+
--color-danger-soft: var(--danger-soft);
|
|
125
|
+
--color-danger-border: var(--danger-border);
|
|
126
|
+
--color-danger-text: var(--danger-text);
|
|
127
|
+
--color-input-bg: var(--input-bg);
|
|
128
|
+
--color-ghost-bg: var(--ghost-bg);
|
|
129
|
+
--color-tag-bg: var(--tag-bg);
|
|
130
|
+
--color-tag-text: var(--tag-text);
|
|
131
|
+
--color-overlay: var(--overlay);
|
|
132
|
+
--color-input-focus-border: var(--input-focus-border);
|
|
133
|
+
--radius-sm: var(--radius-sm);
|
|
134
|
+
--radius-md: var(--radius-md);
|
|
135
|
+
--radius-lg: var(--radius-lg);
|
|
136
|
+
--shadow-sm: var(--shadow-sm);
|
|
137
|
+
--shadow-md: var(--shadow-md);
|
|
138
|
+
--font-sans: var(--font-geist-sans), system-ui, sans-serif;
|
|
139
|
+
--font-mono: var(--font-geist-mono), monospace;
|
|
140
|
+
}
|
package/app/layout.tsx
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import { Geist, Geist_Mono } from 'next/font/google'
|
|
3
|
+
import Script from 'next/script'
|
|
4
|
+
|
|
5
|
+
import { ConfirmDialogProvider } from '@/components/confirm-dialog-provider'
|
|
6
|
+
import { ThemeModeSystemListener } from '@/components/theme-mode-system-listener'
|
|
7
|
+
import { theme_init_script } from '@/lib/theme_init_script'
|
|
8
|
+
import { ui_settings_init_script } from '@/lib/ui_settings_init_script'
|
|
9
|
+
|
|
10
|
+
import './globals.css'
|
|
11
|
+
|
|
12
|
+
const geist_sans = Geist({
|
|
13
|
+
variable: '--font-geist-sans',
|
|
14
|
+
subsets: ['latin'],
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const geist_mono = Geist_Mono({
|
|
18
|
+
variable: '--font-geist-mono',
|
|
19
|
+
subsets: ['latin'],
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export const metadata: Metadata = {
|
|
23
|
+
title: 'super-time-tracker',
|
|
24
|
+
description: 'Web UI for the super-time-tracker CLI time sheets',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function RootLayout({
|
|
28
|
+
children,
|
|
29
|
+
}: Readonly<{
|
|
30
|
+
children: React.ReactNode
|
|
31
|
+
}>) {
|
|
32
|
+
return (
|
|
33
|
+
<html
|
|
34
|
+
lang="en"
|
|
35
|
+
className={`${geist_sans.variable} ${geist_mono.variable} h-full antialiased`}
|
|
36
|
+
suppressHydrationWarning
|
|
37
|
+
>
|
|
38
|
+
<body className="min-h-full font-sans transition-[background-color,color] duration-200">
|
|
39
|
+
<Script
|
|
40
|
+
id="theme-init"
|
|
41
|
+
strategy="beforeInteractive"
|
|
42
|
+
dangerouslySetInnerHTML={{ __html: theme_init_script }}
|
|
43
|
+
/>
|
|
44
|
+
<Script
|
|
45
|
+
id="ui-settings-init"
|
|
46
|
+
strategy="beforeInteractive"
|
|
47
|
+
dangerouslySetInnerHTML={{ __html: ui_settings_init_script }}
|
|
48
|
+
/>
|
|
49
|
+
<ThemeModeSystemListener />
|
|
50
|
+
<ConfirmDialogProvider>{children}</ConfirmDialogProvider>
|
|
51
|
+
</body>
|
|
52
|
+
</html>
|
|
53
|
+
)
|
|
54
|
+
}
|
package/app/page.tsx
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { cookies } from 'next/headers'
|
|
2
|
+
|
|
3
|
+
import { TrackerApp } from '@/components/tracker-app'
|
|
4
|
+
import { get_initial_preferred_sheet_name } from '@/lib/get_initial_preferred_sheet_name'
|
|
5
|
+
import { get_tracker_state } from '@/lib/get_tracker_state'
|
|
6
|
+
import { read_db } from '@/lib/read_db'
|
|
7
|
+
import {
|
|
8
|
+
ACTIVE_SHEET_COOKIE_NAME,
|
|
9
|
+
DEFAULT_SHEET_FIXED_NAME_COOKIE_NAME,
|
|
10
|
+
DEFAULT_SHEET_SESSION_MODE_COOKIE_NAME,
|
|
11
|
+
} from '@/lib/types/ui_settings'
|
|
12
|
+
|
|
13
|
+
export default async function Home() {
|
|
14
|
+
const cookie_store = await cookies()
|
|
15
|
+
const db = await read_db()
|
|
16
|
+
const preferred_sheet = get_initial_preferred_sheet_name(db, {
|
|
17
|
+
session_mode: cookie_store.get(DEFAULT_SHEET_SESSION_MODE_COOKIE_NAME)?.value,
|
|
18
|
+
fixed_sheet_name: cookie_store.get(DEFAULT_SHEET_FIXED_NAME_COOKIE_NAME)?.value,
|
|
19
|
+
last_viewed_sheet: cookie_store.get(ACTIVE_SHEET_COOKIE_NAME)?.value,
|
|
20
|
+
})
|
|
21
|
+
const initial_state = await get_tracker_state(preferred_sheet)
|
|
22
|
+
|
|
23
|
+
return <TrackerApp initial_state={initial_state} />
|
|
24
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ReportingView } from '@/components/reporting-view'
|
|
2
|
+
import { get_reporting_stats } from '@/lib/get_reporting_stats'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Per-sheet time-tracking reporting route.
|
|
6
|
+
*/
|
|
7
|
+
export default async function ReportingPage() {
|
|
8
|
+
const { sourceSheets } = await get_reporting_stats()
|
|
9
|
+
|
|
10
|
+
return <ReportingView source_sheets={sourceSheets} />
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { GeneralSettingsView } from '@/components/general-settings-view'
|
|
2
|
+
import { read_db } from '@/lib/read_db'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Settings index route — general tracker behavior.
|
|
6
|
+
*/
|
|
7
|
+
export default async function SettingsPage() {
|
|
8
|
+
const db = await read_db()
|
|
9
|
+
const sheet_names = db.sheets.map((sheet) => sheet.name)
|
|
10
|
+
|
|
11
|
+
return <GeneralSettingsView sheet_names={sheet_names} />
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TagManagementView } from '@/components/tag-management-view'
|
|
2
|
+
import { collect_tag_stats } from '@/lib/collect_tag_stats'
|
|
3
|
+
import { read_db } from '@/lib/read_db'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tag rename and merge management route.
|
|
7
|
+
*/
|
|
8
|
+
export default async function TagManagementPage() {
|
|
9
|
+
const db = await read_db()
|
|
10
|
+
const initial_tags = collect_tag_stats(db)
|
|
11
|
+
|
|
12
|
+
return <TagManagementView initial_tags={initial_tags} />
|
|
13
|
+
}
|
package/bin/stt-ui.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const { spawn } = require('child_process')
|
|
5
|
+
const { existsSync, cpSync } = require('fs')
|
|
6
|
+
const { join } = require('path')
|
|
7
|
+
|
|
8
|
+
const root = join(__dirname, '..')
|
|
9
|
+
const standalone_dir = join(root, '.next', 'standalone')
|
|
10
|
+
const server_js = join(standalone_dir, 'server.js')
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Copies build artifacts required by the Next.js standalone server.
|
|
14
|
+
*/
|
|
15
|
+
function sync_standalone_assets() {
|
|
16
|
+
const static_src = join(root, '.next', 'static')
|
|
17
|
+
const static_dest = join(standalone_dir, '.next', 'static')
|
|
18
|
+
|
|
19
|
+
if (existsSync(static_src)) {
|
|
20
|
+
cpSync(static_src, static_dest, { recursive: true })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const public_src = join(root, 'public')
|
|
24
|
+
const public_dest = join(standalone_dir, 'public')
|
|
25
|
+
|
|
26
|
+
if (existsSync(public_src)) {
|
|
27
|
+
cpSync(public_src, public_dest, { recursive: true })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!existsSync(server_js)) {
|
|
32
|
+
console.error(
|
|
33
|
+
'Standalone build not found. Run `pnpm build` first, then `stt-ui`.',
|
|
34
|
+
)
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
sync_standalone_assets()
|
|
39
|
+
|
|
40
|
+
const port = process.env.PORT ?? '3000'
|
|
41
|
+
const hostname = process.env.HOSTNAME ?? '127.0.0.1'
|
|
42
|
+
|
|
43
|
+
const child = spawn(process.execPath, [server_js], {
|
|
44
|
+
cwd: standalone_dir,
|
|
45
|
+
env: {
|
|
46
|
+
...process.env,
|
|
47
|
+
NODE_ENV: 'production',
|
|
48
|
+
HOSTNAME: hostname,
|
|
49
|
+
PORT: port,
|
|
50
|
+
},
|
|
51
|
+
stdio: 'inherit',
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
child.on('exit', (code, signal) => {
|
|
55
|
+
if (signal !== null) {
|
|
56
|
+
process.kill(process.pid, signal)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
process.exit(code ?? 0)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
console.error(`stt-ui listening on http://${hostname}:${port}`)
|