pacatui 0.1.14 → 0.1.16

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/README.md CHANGED
@@ -16,6 +16,7 @@ A simple TUI app for task, timer and invoicing for projects.
16
16
  - **Stripe Invoicing** - Create draft invoices directly from time entries
17
17
  - **Invoice Management** - View and manage all your Stripe invoices
18
18
  - **Dashboard** - Overview of projects, tasks, and time stats
19
+ - **Menu Bar** - Native macOS menu bar companion for quick timer control
19
20
  - **Offline-first** - All data stored locally in SQLite
20
21
  - **Vim-style Navigation** - Keyboard-driven interface
21
22
 
@@ -120,6 +121,7 @@ Access settings by pressing `5`:
120
121
  - **Business Name** - Your business name for invoices
121
122
  - **Stripe API Key** - Enable invoicing features
122
123
  - **Timezone** - Set display timezone (or auto-detect)
124
+ - **Menu Bar** - Toggle the macOS menu bar companion
123
125
  - **Export/Import** - Backup and restore your data
124
126
 
125
127
  ### Data Location
@@ -127,6 +129,27 @@ Access settings by pressing `5`:
127
129
  - Database: `~/.paca/paca.db`
128
130
  - Backups: `~/.paca/backups/`
129
131
 
132
+ ## Menu Bar (macOS)
133
+
134
+ Paca includes a native macOS menu bar companion that shows your running timer and lets you start/stop timers without opening the TUI.
135
+
136
+ ### Enable via Settings
137
+
138
+ 1. Press `5` to open Settings
139
+ 2. Select **Menu Bar** and press `Enter` to toggle it on
140
+
141
+ ### Enable via CLI
142
+
143
+ ```bash
144
+ paca menubar enable # Compile & launch
145
+ paca menubar disable # Stop & remove
146
+ paca menubar status # Show current status
147
+ ```
148
+
149
+ The first time you enable it, Paca compiles a small native Swift helper binary (requires Xcode Command Line Tools). The paca mascot icon appears in your menu bar with live timer status.
150
+
151
+ **Requires**: Xcode Command Line Tools (`xcode-select --install`)
152
+
130
153
  ## Stripe Integration
131
154
 
132
155
  To enable invoicing:
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pacatui",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "A simple tui app for task, timer and invoicing for projects.",
5
5
  "module": "src/index.tsx",
6
6
  "type": "module",
package/src/App.tsx CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  EditTimeEntryModal,
21
21
  CreateInvoiceModal,
22
22
  ThemeSelectModal,
23
+ DatabaseSelectModal,
23
24
  } from "./components/index.ts";
24
25
  import { InvoicesView } from "./components/InvoicesView.tsx";
25
26
  import {
@@ -31,7 +32,16 @@ import {
31
32
  database,
32
33
  customers,
33
34
  invoices,
35
+ switchDatabase,
34
36
  } from "./db.ts";
37
+ import {
38
+ getActiveDbFilename,
39
+ setActiveDbFilename,
40
+ getActiveDbPath,
41
+ listDatabases,
42
+ sanitizeDbName,
43
+ getPacaDir,
44
+ } from "./db-path.ts";
35
45
  import { getOrCreateStripeCustomer, createDraftInvoice, listInvoices, clearInvoiceCache, type StripeInvoiceItem } from "./stripe.ts";
36
46
  import {
37
47
  getEffectiveTimezone,
@@ -109,6 +119,7 @@ export function App() {
109
119
  stripeApiKey: "",
110
120
  timezone: "auto",
111
121
  theme: "catppuccin-mocha",
122
+ menuBar: "disabled",
112
123
  });
113
124
 
114
125
  // Timesheet State
@@ -151,6 +162,9 @@ export function App() {
151
162
  const [invoicesHasMore, setInvoicesHasMore] = useState(false);
152
163
  const [invoicesCursors, setInvoicesCursors] = useState<string[]>([]); // Stack of cursors for pagination
153
164
 
165
+ // Database State
166
+ const [currentDbFilename, setCurrentDbFilename] = useState(getActiveDbFilename());
167
+
154
168
  // Modal State
155
169
  const [inputMode, setInputMode] = useState<InputMode | null>(null);
156
170
  const [statusMessage, setStatusMessage] =
@@ -364,6 +378,30 @@ export function App() {
364
378
  loadCustomers();
365
379
  }, []);
366
380
 
381
+ // Poll for external timer changes (e.g. from menu bar)
382
+ // Uses bun:sqlite directly to bypass Prisma's connection cache
383
+ useEffect(() => {
384
+ const poll = setInterval(async () => {
385
+ const { getRunningTimer: getRunningTimerLite } = await import("./menubar/db-lite.ts");
386
+ const row = getRunningTimerLite();
387
+ if (row) {
388
+ setRunningTimer({
389
+ id: row.id,
390
+ startTime: new Date(row.startTime.includes("T") ? row.startTime : row.startTime + "Z"),
391
+ project: {
392
+ id: row.projectId,
393
+ name: row.projectName,
394
+ color: row.projectColor,
395
+ hourlyRate: row.projectHourlyRate,
396
+ },
397
+ });
398
+ } else {
399
+ setRunningTimer(null);
400
+ }
401
+ }, 5000);
402
+ return () => clearInterval(poll);
403
+ }, []);
404
+
367
405
  // Reload tasks when project selection changes
368
406
  useEffect(() => {
369
407
  loadTasks();
@@ -473,14 +511,19 @@ export function App() {
473
511
  return;
474
512
  }
475
513
 
476
- // Handle input mode escape (except select_timer_project which has its own handler)
477
- if (inputMode && inputMode !== "select_timer_project") {
514
+ // Handle input mode escape (except modals that handle their own escape)
515
+ if (inputMode && inputMode !== "select_timer_project" && inputMode !== "select_database" && inputMode !== "create_database_name") {
478
516
  if (key.name === "escape") {
479
517
  setInputMode(null);
480
518
  }
481
519
  return;
482
520
  }
483
521
 
522
+ // Handle select_database and create_database_name modals (they have their own handlers)
523
+ if (inputMode === "select_database" || inputMode === "create_database_name") {
524
+ return;
525
+ }
526
+
484
527
  // Handle select timer project modal
485
528
  if (inputMode === "select_timer_project") {
486
529
  // The modal has its own keyboard handler
@@ -846,6 +889,7 @@ export function App() {
846
889
  }
847
890
 
848
891
  // Select/activate setting
892
+ // Order matches: Business (0: businessName, 1: stripeApiKey) + App (2: database, 3: theme, 4: timezone, 5: menuBar, 6: export, 7: import)
849
893
  if (key.name === "return") {
850
894
  switch (selectedSettingsIndex) {
851
895
  case 0: // Business Name
@@ -854,16 +898,22 @@ export function App() {
854
898
  case 1: // Stripe API Key
855
899
  setInputMode("edit_stripe_key");
856
900
  break;
857
- case 2: // Theme
901
+ case 2: // Database
902
+ setInputMode("select_database");
903
+ break;
904
+ case 3: // Theme
858
905
  setInputMode("select_theme");
859
906
  break;
860
- case 3: // Timezone
907
+ case 4: // Timezone
861
908
  setInputMode("edit_timezone");
862
909
  break;
863
- case 4: // Export Database
910
+ case 5: // Menu Bar
911
+ handleToggleMenuBar();
912
+ break;
913
+ case 6: // Export Database
864
914
  handleExportDatabase();
865
915
  break;
866
- case 5: // Import Database
916
+ case 7: // Import Database
867
917
  setConfirmMessage("Import will replace all data. Continue?");
868
918
  setConfirmAction(() => () => handleImportDatabase());
869
919
  break;
@@ -1132,7 +1182,8 @@ export function App() {
1132
1182
  }
1133
1183
 
1134
1184
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
1135
- const exportPath = `${backupDir}/paca-backup-${timestamp}.db`;
1185
+ const dbName = currentDbFilename.replace(/\.db$/, "");
1186
+ const exportPath = `${backupDir}/${dbName}-backup-${timestamp}.db`;
1136
1187
  try {
1137
1188
  await database.exportToFile(exportPath);
1138
1189
  showMessage(`Exported to ${exportPath}`);
@@ -1177,6 +1228,117 @@ export function App() {
1177
1228
  showMessage(`Theme: ${theme.displayName}`);
1178
1229
  };
1179
1230
 
1231
+ const handleToggleMenuBar = async () => {
1232
+ const newValue = appSettings.menuBar === "enabled" ? "disabled" : "enabled";
1233
+ await settings.set("menuBar", newValue);
1234
+ setAppSettings((prev) => ({ ...prev, menuBar: newValue }));
1235
+
1236
+ const { enableMenuBar, disableMenuBar } = await import("./menubar/index.ts");
1237
+ const msg = newValue === "enabled"
1238
+ ? await enableMenuBar()
1239
+ : await disableMenuBar();
1240
+ showMessage(msg);
1241
+ };
1242
+
1243
+ // Database handlers
1244
+ const handleSwitchDatabase = async (filename: string) => {
1245
+ if (filename === currentDbFilename) {
1246
+ setInputMode(null);
1247
+ return;
1248
+ }
1249
+
1250
+ try {
1251
+ const { join } = await import("path");
1252
+ const { existsSync } = await import("fs");
1253
+ const dbPath = join(getPacaDir(), filename);
1254
+
1255
+ // Run migration if database file doesn't exist yet
1256
+ if (!existsSync(dbPath)) {
1257
+ const { execSync } = await import("child_process");
1258
+ const { dirname } = await import("path");
1259
+ const projectDir = dirname(dirname(import.meta.path));
1260
+ execSync(`cd "${projectDir}" && bunx prisma migrate deploy`, {
1261
+ stdio: "pipe",
1262
+ env: {
1263
+ ...process.env,
1264
+ DATABASE_URL: `file:${dbPath}`,
1265
+ },
1266
+ });
1267
+ }
1268
+
1269
+ // Disable menu bar before switching (it points at the old db)
1270
+ if (appSettings.menuBar === "enabled") {
1271
+ const { disableMenuBar } = await import("./menubar/index.ts");
1272
+ await disableMenuBar();
1273
+ await settings.set("menuBar", "disabled");
1274
+ setAppSettings((prev) => ({ ...prev, menuBar: "disabled" }));
1275
+ }
1276
+
1277
+ // Write .active file
1278
+ setActiveDbFilename(filename);
1279
+
1280
+ // Swap PrismaClient
1281
+ await switchDatabase(dbPath);
1282
+
1283
+ // Update state
1284
+ setCurrentDbFilename(filename);
1285
+
1286
+ // Reload all app state
1287
+ const [settingsData, projectData, dashboardData, recentData, weeklyData, runningData, customerData] = await Promise.all([
1288
+ settings.getAppSettings(),
1289
+ projects.getAll(false),
1290
+ stats.getDashboardStats(),
1291
+ stats.getRecentActivity(10),
1292
+ stats.getWeeklyTimeStats(6),
1293
+ timeEntries.getRunning(),
1294
+ customers.getAll(),
1295
+ ]);
1296
+
1297
+ setAppSettings(settingsData);
1298
+ setProjectList(projectData);
1299
+ setDashboardStats(dashboardData);
1300
+ setRecentTasks(recentData as (Task & { project: { name: string; color: string } })[]);
1301
+ setWeeklyTimeData(weeklyData);
1302
+ setCustomerList(customerData);
1303
+
1304
+ if (runningData) {
1305
+ setRunningTimer({
1306
+ id: runningData.id,
1307
+ startTime: runningData.startTime,
1308
+ project: runningData.project,
1309
+ });
1310
+ } else {
1311
+ setRunningTimer(null);
1312
+ }
1313
+
1314
+ // Reset selection indices
1315
+ setSelectedProjectIndex(0);
1316
+ setSelectedTaskIndex(0);
1317
+ setSelectedDashboardTaskIndex(0);
1318
+ setSelectedSettingsIndex(0);
1319
+
1320
+ setInputMode(null);
1321
+ showMessage(`Switched to database: ${filename.replace(/\.db$/, "")}`);
1322
+ } catch (error) {
1323
+ showMessage(`Failed to switch database: ${error}`);
1324
+ }
1325
+ };
1326
+
1327
+ const handleCreateDatabase = async (name: string) => {
1328
+ const filename = sanitizeDbName(name);
1329
+ const { join } = await import("path");
1330
+ const { existsSync } = await import("fs");
1331
+ const dbPath = join(getPacaDir(), filename);
1332
+
1333
+ if (existsSync(dbPath)) {
1334
+ showMessage(`Database "${filename.replace(/\.db$/, "")}" already exists`);
1335
+ setInputMode("select_database");
1336
+ return;
1337
+ }
1338
+
1339
+ await handleSwitchDatabase(filename);
1340
+ };
1341
+
1180
1342
  // Modal handlers
1181
1343
  const handleCreateProject = async (name: string, rate: number | null) => {
1182
1344
  await projects.create({ name, hourlyRate: rate });
@@ -1433,10 +1595,13 @@ export function App() {
1433
1595
  settings={appSettings}
1434
1596
  selectedIndex={selectedSettingsIndex}
1435
1597
  theme={getTheme(appSettings.theme)}
1598
+ currentDbFilename={currentDbFilename}
1599
+ onSelectDatabase={() => setInputMode("select_database")}
1436
1600
  onEditBusinessName={() => setInputMode("edit_business_name")}
1437
1601
  onEditStripeKey={() => setInputMode("edit_stripe_key")}
1438
1602
  onEditTimezone={() => setInputMode("edit_timezone")}
1439
1603
  onSelectTheme={() => setInputMode("select_theme")}
1604
+ onToggleMenuBar={handleToggleMenuBar}
1440
1605
  onExportDatabase={handleExportDatabase}
1441
1606
  onImportDatabase={() => {
1442
1607
  setConfirmMessage("Import will replace all data. Continue?");
@@ -1626,6 +1791,28 @@ export function App() {
1626
1791
  />
1627
1792
  )}
1628
1793
 
1794
+ {inputMode === "select_database" && (
1795
+ <DatabaseSelectModal
1796
+ databases={listDatabases()}
1797
+ currentDatabase={currentDbFilename}
1798
+ onSelect={handleSwitchDatabase}
1799
+ onCreateNew={() => setInputMode("create_database_name")}
1800
+ onCancel={() => setInputMode(null)}
1801
+ theme={theme}
1802
+ />
1803
+ )}
1804
+
1805
+ {inputMode === "create_database_name" && (
1806
+ <InputModal
1807
+ mode={inputMode}
1808
+ title="Create New Database"
1809
+ placeholder="Database name (e.g. work, personal)..."
1810
+ onSubmit={handleCreateDatabase}
1811
+ onCancel={() => setInputMode("select_database")}
1812
+ theme={theme}
1813
+ />
1814
+ )}
1815
+
1629
1816
  {inputMode === "select_timer_project" && (
1630
1817
  <ProjectSelectModal
1631
1818
  projects={projectList.filter((p) => !p.archived)}
@@ -0,0 +1,124 @@
1
+ import { useState } from "react";
2
+ import { useKeyboard } from "@opentui/react";
3
+ import type { Theme } from "../types.ts";
4
+ import Modal from "./Modal.tsx";
5
+
6
+ interface DatabaseSelectModalProps {
7
+ databases: string[];
8
+ currentDatabase: string;
9
+ onSelect: (filename: string) => void;
10
+ onCreateNew: () => void;
11
+ onCancel: () => void;
12
+ theme: Theme;
13
+ }
14
+
15
+ export function DatabaseSelectModal({
16
+ databases,
17
+ currentDatabase,
18
+ onSelect,
19
+ onCreateNew,
20
+ onCancel,
21
+ theme,
22
+ }: DatabaseSelectModalProps) {
23
+ const currentIndex = databases.indexOf(currentDatabase);
24
+ const [selectedIndex, setSelectedIndex] = useState(
25
+ currentIndex >= 0 ? currentIndex : 0,
26
+ );
27
+
28
+ const colors = theme.colors;
29
+
30
+ // Total items = databases + "Create new" option
31
+ const totalItems = databases.length + 1;
32
+
33
+ useKeyboard((key) => {
34
+ if (key.name === "escape") {
35
+ onCancel();
36
+ return;
37
+ }
38
+ if (key.name === "return") {
39
+ if (selectedIndex === databases.length) {
40
+ onCreateNew();
41
+ } else {
42
+ const selected = databases[selectedIndex];
43
+ if (selected) {
44
+ onSelect(selected);
45
+ }
46
+ }
47
+ return;
48
+ }
49
+ if (key.name === "down" || key.name === "j") {
50
+ setSelectedIndex((i) => Math.min(i + 1, totalItems - 1));
51
+ return;
52
+ }
53
+ if (key.name === "up" || key.name === "k") {
54
+ setSelectedIndex((i) => Math.max(i - 1, 0));
55
+ return;
56
+ }
57
+ });
58
+
59
+ // border(2) + padding(2) + title(1) + marginTop(1) + items + spacer(1) + create_new(1) + marginTop(1) + help(1)
60
+ const height = Math.min(totalItems + 10, 20);
61
+
62
+ return (
63
+ <Modal title="Select Database" height={height} theme={theme}>
64
+ <box style={{ flexGrow: 1, marginTop: 1 }}>
65
+ {databases.map((db, index) => {
66
+ const isSelected = index === selectedIndex;
67
+ const isCurrent = db === currentDatabase;
68
+ const displayName = db.replace(/\.db$/, "");
69
+
70
+ return (
71
+ <box
72
+ key={db}
73
+ style={{
74
+ paddingLeft: 1,
75
+ paddingRight: 1,
76
+ backgroundColor: isSelected ? colors.selectedRowBg : "transparent",
77
+ }}
78
+ >
79
+ <text>
80
+ <span fg={isSelected ? colors.selectedText : colors.textPrimary}>
81
+ {isSelected ? "▸ " : " "}
82
+ </span>
83
+ <span
84
+ fg={isSelected ? colors.selectedText : colors.textPrimary}
85
+ attributes={isSelected ? "bold" : undefined}
86
+ >
87
+ {displayName}
88
+ </span>
89
+ {isCurrent && (
90
+ <span fg={colors.success}> (active)</span>
91
+ )}
92
+ </text>
93
+ </box>
94
+ );
95
+ })}
96
+
97
+ {/* Create new option */}
98
+ <box
99
+ style={{
100
+ paddingLeft: 1,
101
+ paddingRight: 1,
102
+ marginTop: databases.length > 0 ? 1 : 0,
103
+ backgroundColor: selectedIndex === databases.length ? colors.selectedRowBg : "transparent",
104
+ }}
105
+ >
106
+ <text>
107
+ <span fg={selectedIndex === databases.length ? colors.selectedText : colors.accent}>
108
+ {selectedIndex === databases.length ? "▸ " : " "}
109
+ </span>
110
+ <span
111
+ fg={selectedIndex === databases.length ? colors.selectedText : colors.accent}
112
+ attributes={selectedIndex === databases.length ? "bold" : undefined}
113
+ >
114
+ + Create new database...
115
+ </span>
116
+ </text>
117
+ </box>
118
+ </box>
119
+ <text fg={colors.textMuted} style={{ marginTop: 1 }}>
120
+ ↑/↓ to navigate, Enter to select, Esc to cancel
121
+ </text>
122
+ </Modal>
123
+ );
124
+ }
@@ -5,31 +5,46 @@ interface SettingsViewProps {
5
5
  settings: AppSettings;
6
6
  selectedIndex: number;
7
7
  theme: Theme;
8
+ currentDbFilename: string;
9
+ onSelectDatabase: () => void;
8
10
  onEditBusinessName: () => void;
9
11
  onEditStripeKey: () => void;
10
12
  onEditTimezone: () => void;
11
13
  onSelectTheme: () => void;
14
+ onToggleMenuBar: () => void;
12
15
  onExportDatabase: () => void;
13
16
  onImportDatabase: () => void;
14
17
  }
15
18
 
16
- const SETTINGS_ITEMS = [
19
+ type SettingsItem = { key: string; label: string; type: string };
20
+
21
+ const BUSINESS_SETTINGS: SettingsItem[] = [
17
22
  { key: "businessName", label: "Business Name", type: "text" },
18
23
  { key: "stripeApiKey", label: "Stripe API Key", type: "secret" },
24
+ ];
25
+
26
+ const APP_SETTINGS: SettingsItem[] = [
27
+ { key: "database", label: "Database", type: "select" },
19
28
  { key: "theme", label: "Theme", type: "select" },
20
29
  { key: "timezone", label: "Timezone", type: "text" },
30
+ { key: "menuBar", label: "Menu Bar", type: "toggle" },
21
31
  { key: "exportDatabase", label: "Export Database", type: "action" },
22
32
  { key: "importDatabase", label: "Import Database", type: "action" },
23
- ] as const;
33
+ ];
34
+
35
+ const ALL_SETTINGS = [...BUSINESS_SETTINGS, ...APP_SETTINGS];
24
36
 
25
37
  export function SettingsView({
26
38
  settings,
27
39
  selectedIndex,
28
40
  theme,
41
+ currentDbFilename,
42
+ onSelectDatabase,
29
43
  onEditBusinessName,
30
44
  onEditStripeKey,
31
45
  onEditTimezone,
32
46
  onSelectTheme,
47
+ onToggleMenuBar,
33
48
  onExportDatabase,
34
49
  onImportDatabase,
35
50
  }: SettingsViewProps) {
@@ -55,6 +70,8 @@ export function SettingsView({
55
70
 
56
71
  const getValue = (key: string) => {
57
72
  switch (key) {
73
+ case "database":
74
+ return currentDbFilename.replace(/\.db$/, "");
58
75
  case "theme":
59
76
  return getThemeDisplay();
60
77
  case "businessName":
@@ -63,6 +80,8 @@ export function SettingsView({
63
80
  return maskSecret(settings.stripeApiKey);
64
81
  case "timezone":
65
82
  return formatTimezone(settings.timezone);
83
+ case "menuBar":
84
+ return settings.menuBar === "enabled" ? "Enabled" : "Disabled";
66
85
  case "exportDatabase":
67
86
  return "Export to file...";
68
87
  case "importDatabase":
@@ -74,6 +93,8 @@ export function SettingsView({
74
93
 
75
94
  const getAction = (key: string) => {
76
95
  switch (key) {
96
+ case "database":
97
+ return onSelectDatabase;
77
98
  case "theme":
78
99
  return onSelectTheme;
79
100
  case "businessName":
@@ -82,6 +103,8 @@ export function SettingsView({
82
103
  return onEditStripeKey;
83
104
  case "timezone":
84
105
  return onEditTimezone;
106
+ case "menuBar":
107
+ return onToggleMenuBar;
85
108
  case "exportDatabase":
86
109
  return onExportDatabase;
87
110
  case "importDatabase":
@@ -94,9 +117,42 @@ export function SettingsView({
94
117
  const getActionLabel = (type: string) => {
95
118
  if (type === "action") return "run";
96
119
  if (type === "select") return "select";
120
+ if (type === "toggle") return "toggle";
97
121
  return "edit";
98
122
  };
99
123
 
124
+ const renderItem = (item: SettingsItem, globalIndex: number) => {
125
+ const isSelected = globalIndex === selectedIndex;
126
+ return (
127
+ <box
128
+ key={item.key}
129
+ style={{
130
+ flexDirection: "row",
131
+ paddingLeft: 1,
132
+ paddingRight: 1,
133
+ backgroundColor: isSelected ? colors.selectedRowBg : "transparent",
134
+ }}
135
+ >
136
+ <box style={{ width: 20 }}>
137
+ <text
138
+ fg={isSelected ? colors.selectedText : colors.textPrimary}
139
+ attributes={isSelected ? "bold" : undefined}
140
+ >
141
+ {item.label}
142
+ </text>
143
+ </box>
144
+ <box style={{ flexGrow: 1 }}>
145
+ <text fg={isSelected ? colors.selectedText : colors.textSecondary}>
146
+ {getValue(item.key)}
147
+ </text>
148
+ </box>
149
+ {isSelected && (
150
+ <text fg={colors.accent}>[Enter to {getActionLabel(item.type)}]</text>
151
+ )}
152
+ </box>
153
+ );
154
+ };
155
+
100
156
  return (
101
157
  <box
102
158
  style={{
@@ -105,50 +161,31 @@ export function SettingsView({
105
161
  padding: 1,
106
162
  }}
107
163
  >
108
- <box style={{ marginBottom: 1 }}>
109
- <text fg={colors.accent}>Application Settings</text>
164
+ <box
165
+ title="Business"
166
+ style={{
167
+ border: true,
168
+ borderColor: colors.borderSubtle,
169
+ padding: 1,
170
+ flexDirection: "column",
171
+ }}
172
+ >
173
+ {BUSINESS_SETTINGS.map((item, i) => renderItem(item, i))}
110
174
  </box>
111
175
 
112
176
  <box
113
- title="Settings"
177
+ title="App"
114
178
  style={{
115
179
  border: true,
116
180
  borderColor: colors.borderSubtle,
117
181
  padding: 1,
118
182
  flexDirection: "column",
183
+ marginTop: 1,
119
184
  }}
120
185
  >
121
- {SETTINGS_ITEMS.map((item, index) => {
122
- const isSelected = index === selectedIndex;
123
- return (
124
- <box
125
- key={item.key}
126
- style={{
127
- flexDirection: "row",
128
- paddingLeft: 1,
129
- paddingRight: 1,
130
- backgroundColor: isSelected ? colors.selectedRowBg : "transparent",
131
- }}
132
- >
133
- <box style={{ width: 20 }}>
134
- <text
135
- fg={isSelected ? colors.selectedText : colors.textPrimary}
136
- attributes={isSelected ? "bold" : undefined}
137
- >
138
- {item.label}
139
- </text>
140
- </box>
141
- <box style={{ flexGrow: 1 }}>
142
- <text fg={isSelected ? colors.selectedText : colors.textSecondary}>
143
- {getValue(item.key)}
144
- </text>
145
- </box>
146
- {isSelected && (
147
- <text fg={colors.accent}>[Enter to {getActionLabel(item.type)}]</text>
148
- )}
149
- </box>
150
- );
151
- })}
186
+ {APP_SETTINGS.map((item, i) =>
187
+ renderItem(item, BUSINESS_SETTINGS.length + i),
188
+ )}
152
189
  </box>
153
190
 
154
191
  <box style={{ marginTop: 2 }}>
@@ -158,10 +195,10 @@ export function SettingsView({
158
195
  </box>
159
196
 
160
197
  <box style={{ marginTop: 1 }}>
161
- <text fg={colors.textMuted}>Database location: ~/.paca/paca.db</text>
198
+ <text fg={colors.textMuted}>Database: ~/.paca/{currentDbFilename}</text>
162
199
  </box>
163
200
  </box>
164
201
  );
165
202
  }
166
203
 
167
- export const SETTINGS_COUNT = SETTINGS_ITEMS.length;
204
+ export const SETTINGS_COUNT = ALL_SETTINGS.length;
@@ -15,3 +15,4 @@ export { CustomerSelectModal } from "./CustomerSelectModal.tsx";
15
15
  export { EditTimeEntryModal } from "./EditTimeEntryModal.tsx";
16
16
  export { CreateInvoiceModal } from "./CreateInvoiceModal.tsx";
17
17
  export { ThemeSelectModal } from "./ThemeSelectModal.tsx";
18
+ export { DatabaseSelectModal } from "./DatabaseSelectModal.tsx";