pacatui 0.1.5 → 0.1.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pacatui",
3
- "version": "0.1.5",
3
+ "version": "0.1.11",
4
4
  "description": "A simple tui app for task, timer and invoicing for projects.",
5
5
  "module": "src/index.tsx",
6
6
  "type": "module",
@@ -66,8 +66,6 @@
66
66
  "@prisma/adapter-libsql": "^7.2.0",
67
67
  "@prisma/client": "^7.2.0",
68
68
  "better-sqlite3": "^12.6.0",
69
- "paca": "^1.0.10",
70
- "pacatui": "^0.1.1",
71
69
  "prisma": "^7.2.0",
72
70
  "react": "^19.2.3",
73
71
  "stripe": "^20.2.0"
package/src/App.tsx CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  CustomerSelectModal,
20
20
  EditTimeEntryModal,
21
21
  CreateInvoiceModal,
22
+ ThemeSelectModal,
22
23
  } from "./components/index.ts";
23
24
  import { InvoicesView } from "./components/InvoicesView.tsx";
24
25
  import {
@@ -34,6 +35,8 @@ import {
34
35
  import { getOrCreateStripeCustomer, createDraftInvoice, listInvoices, clearInvoiceCache, type StripeInvoiceItem } from "./stripe.ts";
35
36
  import {
36
37
  getEffectiveTimezone,
38
+ formatDateInTimezone,
39
+ getTheme,
37
40
  type View,
38
41
  type Panel,
39
42
  type InputMode,
@@ -47,6 +50,7 @@ import {
47
50
  type TimeEntry,
48
51
  type TimeEntryWithProject,
49
52
  type WeeklyTimeData,
53
+ type AllTimersWeekData,
50
54
  } from "./types.ts";
51
55
 
52
56
  function formatDuration(ms: number): string {
@@ -104,6 +108,7 @@ export function App() {
104
108
  businessName: "",
105
109
  stripeApiKey: "",
106
110
  timezone: "auto",
111
+ theme: "catppuccin-mocha",
107
112
  });
108
113
 
109
114
  // Timesheet State
@@ -126,6 +131,13 @@ export function App() {
126
131
  | null
127
132
  >(null);
128
133
 
134
+ // All Timers State (for viewing all timers paged by week)
135
+ const [showAllTimers, setShowAllTimers] = useState(false);
136
+ const [allTimersWeekData, setAllTimersWeekData] = useState<AllTimersWeekData | null>(null);
137
+ const [allTimersSelectedIndex, setAllTimersSelectedIndex] = useState(0);
138
+ const [hasOlderWeeks, setHasOlderWeeks] = useState(false);
139
+ const [hasNewerWeeks, setHasNewerWeeks] = useState(false);
140
+
129
141
  // Customer State
130
142
  const [customerList, setCustomerList] = useState<Customer[]>([]);
131
143
  const [editingCustomer, setEditingCustomer] = useState<Customer | null>(null);
@@ -293,6 +305,56 @@ export function App() {
293
305
  }
294
306
  }, [selectedTimesheetGroupIndex, selectedTimeEntryIndex]);
295
307
 
308
+ // Helper function to get the start of a week (Sunday)
309
+ const getWeekStart = useCallback((date: Date): Date => {
310
+ const d = new Date(date);
311
+ d.setHours(0, 0, 0, 0);
312
+ d.setDate(d.getDate() - d.getDay());
313
+ return d;
314
+ }, []);
315
+
316
+ // Load all timers for a specific week
317
+ const loadAllTimers = useCallback(async (weekStart?: Date) => {
318
+ const currentWeekStart = weekStart ?? getWeekStart(new Date());
319
+ const weekEnd = new Date(currentWeekStart);
320
+ weekEnd.setDate(weekEnd.getDate() + 7);
321
+
322
+ const entries = await timeEntries.getAllForWeek(currentWeekStart);
323
+
324
+ // Calculate total duration
325
+ let totalMs = 0;
326
+ for (const entry of entries) {
327
+ if (entry.endTime) {
328
+ totalMs += new Date(entry.endTime).getTime() - new Date(entry.startTime).getTime();
329
+ }
330
+ }
331
+
332
+ setAllTimersWeekData({
333
+ weekStart: currentWeekStart,
334
+ weekEnd,
335
+ entries: entries as TimeEntryWithProject[],
336
+ totalMs,
337
+ });
338
+
339
+ // Check if there are older weeks
340
+ const oldestDate = await timeEntries.getOldestEntryDate();
341
+ if (oldestDate) {
342
+ const oldestWeekStart = getWeekStart(new Date(oldestDate));
343
+ setHasOlderWeeks(oldestWeekStart < currentWeekStart);
344
+ } else {
345
+ setHasOlderWeeks(false);
346
+ }
347
+
348
+ // Check if there are newer weeks (current week start is before today's week start)
349
+ const todayWeekStart = getWeekStart(new Date());
350
+ setHasNewerWeeks(currentWeekStart < todayWeekStart);
351
+
352
+ // Adjust selection if needed
353
+ if (allTimersSelectedIndex >= entries.length) {
354
+ setAllTimersSelectedIndex(Math.max(0, entries.length - 1));
355
+ }
356
+ }, [getWeekStart, allTimersSelectedIndex]);
357
+
296
358
  // Initial load
297
359
  useEffect(() => {
298
360
  loadProjects();
@@ -317,9 +379,13 @@ export function App() {
317
379
  // Reload timesheets when on timesheets view
318
380
  useEffect(() => {
319
381
  if (currentView === "timesheets") {
320
- loadTimesheets();
382
+ if (showAllTimers) {
383
+ loadAllTimers(allTimersWeekData?.weekStart);
384
+ } else {
385
+ loadTimesheets();
386
+ }
321
387
  }
322
- }, [currentView]);
388
+ }, [currentView, showAllTimers]);
323
389
 
324
390
  // Load invoices when on invoices view
325
391
  useEffect(() => {
@@ -384,7 +450,11 @@ export function App() {
384
450
 
385
451
  // Refresh timesheets if currently viewing them
386
452
  if (currentView === "timesheets") {
387
- loadTimesheets();
453
+ if (showAllTimers) {
454
+ loadAllTimers(allTimersWeekData?.weekStart);
455
+ } else {
456
+ loadTimesheets();
457
+ }
388
458
  }
389
459
  };
390
460
 
@@ -784,13 +854,16 @@ export function App() {
784
854
  case 1: // Stripe API Key
785
855
  setInputMode("edit_stripe_key");
786
856
  break;
787
- case 2: // Timezone
857
+ case 2: // Theme
858
+ setInputMode("select_theme");
859
+ break;
860
+ case 3: // Timezone
788
861
  setInputMode("edit_timezone");
789
862
  break;
790
- case 3: // Export Database
863
+ case 4: // Export Database
791
864
  handleExportDatabase();
792
865
  break;
793
- case 4: // Import Database
866
+ case 5: // Import Database
794
867
  setConfirmMessage("Import will replace all data. Continue?");
795
868
  setConfirmAction(() => () => handleImportDatabase());
796
869
  break;
@@ -806,6 +879,84 @@ export function App() {
806
879
  };
807
880
 
808
881
  const handleTimesheetKeyboard = (key: { name: string }) => {
882
+ // Toggle between default timesheets and all timers view
883
+ if (key.name === "a") {
884
+ setShowAllTimers((prev) => {
885
+ const newValue = !prev;
886
+ if (newValue) {
887
+ showMessage("Showing all timers by week");
888
+ loadAllTimers();
889
+ } else {
890
+ showMessage("Showing billable timesheets");
891
+ loadTimesheets();
892
+ }
893
+ return newValue;
894
+ });
895
+ return;
896
+ }
897
+
898
+ // Handle all timers mode
899
+ if (showAllTimers) {
900
+ if (!allTimersWeekData) return;
901
+ const maxIndex = allTimersWeekData.entries.length - 1;
902
+
903
+ // Navigation within entries
904
+ if (key.name === "j" || key.name === "down") {
905
+ setAllTimersSelectedIndex((i) => Math.min(i + 1, maxIndex));
906
+ return;
907
+ }
908
+ if (key.name === "k" || key.name === "up") {
909
+ setAllTimersSelectedIndex((i) => Math.max(i - 1, 0));
910
+ return;
911
+ }
912
+
913
+ // Week navigation
914
+ if (key.name === "[" && hasOlderWeeks) {
915
+ const prevWeekStart = new Date(allTimersWeekData.weekStart);
916
+ prevWeekStart.setDate(prevWeekStart.getDate() - 7);
917
+ loadAllTimers(prevWeekStart);
918
+ setAllTimersSelectedIndex(0);
919
+ return;
920
+ }
921
+ if (key.name === "]" && hasNewerWeeks) {
922
+ const nextWeekStart = new Date(allTimersWeekData.weekStart);
923
+ nextWeekStart.setDate(nextWeekStart.getDate() + 7);
924
+ loadAllTimers(nextWeekStart);
925
+ setAllTimersSelectedIndex(0);
926
+ return;
927
+ }
928
+
929
+ // Edit entry
930
+ if (key.name === "e") {
931
+ const entry = allTimersWeekData.entries[allTimersSelectedIndex];
932
+ if (entry) {
933
+ setEditingTimeEntry(entry as typeof editingTimeEntry);
934
+ setInputMode("edit_time_entry");
935
+ }
936
+ return;
937
+ }
938
+
939
+ // Delete entry
940
+ if (key.name === "d") {
941
+ const entry = allTimersWeekData.entries[allTimersSelectedIndex];
942
+ if (entry) {
943
+ setConfirmMessage(
944
+ `Delete time entry from ${entry.project.name}?`,
945
+ );
946
+ setConfirmAction(() => () => {
947
+ timeEntries.delete(entry.id).then(() => {
948
+ showMessage("Time entry deleted");
949
+ loadAllTimers(allTimersWeekData.weekStart);
950
+ });
951
+ });
952
+ }
953
+ return;
954
+ }
955
+
956
+ return;
957
+ }
958
+
959
+ // Default timesheets mode
809
960
  const currentGroup = timesheetGroups[selectedTimesheetGroupIndex];
810
961
  if (!currentGroup) return;
811
962
 
@@ -1018,6 +1169,14 @@ export function App() {
1018
1169
  showMessage(`Timezone set to ${value === "auto" ? "auto-detect" : value}`);
1019
1170
  };
1020
1171
 
1172
+ const handleSelectTheme = async (themeName: string) => {
1173
+ await settings.set("theme", themeName);
1174
+ setAppSettings((prev) => ({ ...prev, theme: themeName }));
1175
+ const theme = getTheme(themeName);
1176
+ setInputMode(null);
1177
+ showMessage(`Theme: ${theme.displayName}`);
1178
+ };
1179
+
1021
1180
  // Modal handlers
1022
1181
  const handleCreateProject = async (name: string, rate: number | null) => {
1023
1182
  await projects.create({ name, hourlyRate: rate });
@@ -1134,7 +1293,11 @@ export function App() {
1134
1293
  showMessage("Time entry updated");
1135
1294
  setInputMode(null);
1136
1295
  setEditingTimeEntry(null);
1137
- loadTimesheets();
1296
+ if (showAllTimers) {
1297
+ loadAllTimers(allTimersWeekData?.weekStart);
1298
+ } else {
1299
+ loadTimesheets();
1300
+ }
1138
1301
  };
1139
1302
 
1140
1303
  const handleCreateInvoice = async () => {
@@ -1232,6 +1395,7 @@ export function App() {
1232
1395
  };
1233
1396
 
1234
1397
  const selectedProject = projectList[selectedProjectIndex];
1398
+ const theme = getTheme(appSettings.theme);
1235
1399
 
1236
1400
  return (
1237
1401
  <box
@@ -1239,6 +1403,7 @@ export function App() {
1239
1403
  width: "100%",
1240
1404
  height: "100%",
1241
1405
  flexDirection: "column",
1406
+ backgroundColor: theme.colors.bg,
1242
1407
  }}
1243
1408
  >
1244
1409
  <Header
@@ -1246,6 +1411,7 @@ export function App() {
1246
1411
  onViewChange={setCurrentView}
1247
1412
  runningTimer={runningTimer}
1248
1413
  onStopTimer={() => setInputMode("stop_timer")}
1414
+ theme={getTheme(appSettings.theme)}
1249
1415
  />
1250
1416
 
1251
1417
  <box style={{ flexGrow: 1, flexDirection: "row" }}>
@@ -1256,18 +1422,21 @@ export function App() {
1256
1422
  weeklyTimeData={weeklyTimeData}
1257
1423
  selectedIndex={selectedDashboardTaskIndex}
1258
1424
  focused={true}
1425
+ theme={getTheme(appSettings.theme)}
1259
1426
  />
1260
1427
  )}
1261
1428
 
1262
- {currentView === "help" && <HelpView />}
1429
+ {currentView === "help" && <HelpView theme={getTheme(appSettings.theme)} />}
1263
1430
 
1264
1431
  {currentView === "settings" && (
1265
1432
  <SettingsView
1266
1433
  settings={appSettings}
1267
1434
  selectedIndex={selectedSettingsIndex}
1435
+ theme={getTheme(appSettings.theme)}
1268
1436
  onEditBusinessName={() => setInputMode("edit_business_name")}
1269
1437
  onEditStripeKey={() => setInputMode("edit_stripe_key")}
1270
1438
  onEditTimezone={() => setInputMode("edit_timezone")}
1439
+ onSelectTheme={() => setInputMode("select_theme")}
1271
1440
  onExportDatabase={handleExportDatabase}
1272
1441
  onImportDatabase={() => {
1273
1442
  setConfirmMessage("Import will replace all data. Continue?");
@@ -1284,6 +1453,12 @@ export function App() {
1284
1453
  selectedEntryIds={selectedTimeEntryIds}
1285
1454
  focused={true}
1286
1455
  timezone={getEffectiveTimezone(appSettings)}
1456
+ theme={getTheme(appSettings.theme)}
1457
+ showAllTimers={showAllTimers}
1458
+ allTimersWeekData={allTimersWeekData}
1459
+ allTimersSelectedIndex={allTimersSelectedIndex}
1460
+ hasOlderWeeks={hasOlderWeeks}
1461
+ hasNewerWeeks={hasNewerWeeks}
1287
1462
  />
1288
1463
  )}
1289
1464
 
@@ -1298,6 +1473,7 @@ export function App() {
1298
1473
  currentPage={invoicesPage}
1299
1474
  hasMore={invoicesHasMore}
1300
1475
  hasPrevious={invoicesCursors.length > 0}
1476
+ theme={getTheme(appSettings.theme)}
1301
1477
  />
1302
1478
  )}
1303
1479
 
@@ -1309,6 +1485,7 @@ export function App() {
1309
1485
  selectedIndex={selectedProjectIndex}
1310
1486
  focused={activePanel === "projects"}
1311
1487
  showArchived={showArchived}
1488
+ theme={getTheme(appSettings.theme)}
1312
1489
  />
1313
1490
  </box>
1314
1491
  <box style={{ width: "60%", flexDirection: "column" }}>
@@ -1317,6 +1494,7 @@ export function App() {
1317
1494
  selectedIndex={selectedTaskIndex}
1318
1495
  focused={activePanel === "tasks"}
1319
1496
  projectName={selectedProject?.name}
1497
+ theme={getTheme(appSettings.theme)}
1320
1498
  />
1321
1499
  </box>
1322
1500
  </>
@@ -1329,6 +1507,22 @@ export function App() {
1329
1507
  timerRunning={!!runningTimer}
1330
1508
  currentView={currentView}
1331
1509
  activePanel={activePanel}
1510
+ theme={getTheme(appSettings.theme)}
1511
+ showAllTimers={currentView === "timesheets" ? showAllTimers : undefined}
1512
+ allTimersWeekRange={
1513
+ currentView === "timesheets" && showAllTimers && allTimersWeekData
1514
+ ? (() => {
1515
+ const tz = getEffectiveTimezone(appSettings);
1516
+ const startStr = formatDateInTimezone(allTimersWeekData.weekStart, tz);
1517
+ const endDate = new Date(allTimersWeekData.weekEnd);
1518
+ endDate.setDate(endDate.getDate() - 1);
1519
+ const endStr = formatDateInTimezone(endDate, tz);
1520
+ return `${startStr} - ${endStr}`;
1521
+ })()
1522
+ : undefined
1523
+ }
1524
+ hasOlderWeeks={currentView === "timesheets" && showAllTimers ? hasOlderWeeks : undefined}
1525
+ hasNewerWeeks={currentView === "timesheets" && showAllTimers ? hasNewerWeeks : undefined}
1332
1526
  />
1333
1527
 
1334
1528
  {/* Modals */}
@@ -1337,6 +1531,7 @@ export function App() {
1337
1531
  mode="create"
1338
1532
  onSubmit={handleCreateProject}
1339
1533
  onCancel={() => setInputMode(null)}
1534
+ theme={theme}
1340
1535
  />
1341
1536
  )}
1342
1537
 
@@ -1347,6 +1542,7 @@ export function App() {
1347
1542
  initialRate={selectedProject.hourlyRate}
1348
1543
  onSubmit={handleEditProject}
1349
1544
  onCancel={() => setInputMode(null)}
1545
+ theme={theme}
1350
1546
  />
1351
1547
  )}
1352
1548
 
@@ -1357,6 +1553,7 @@ export function App() {
1357
1553
  placeholder="Task title..."
1358
1554
  onSubmit={handleCreateTask}
1359
1555
  onCancel={() => setInputMode(null)}
1556
+ theme={theme}
1360
1557
  />
1361
1558
  )}
1362
1559
 
@@ -1368,6 +1565,7 @@ export function App() {
1368
1565
  placeholder="Task title..."
1369
1566
  onSubmit={handleEditTask}
1370
1567
  onCancel={() => setInputMode(null)}
1568
+ theme={theme}
1371
1569
  />
1372
1570
  )}
1373
1571
 
@@ -1380,6 +1578,7 @@ export function App() {
1380
1578
  placeholder="Task title..."
1381
1579
  onSubmit={handleEditDashboardTask}
1382
1580
  onCancel={() => setInputMode(null)}
1581
+ theme={theme}
1383
1582
  />
1384
1583
  )}
1385
1584
 
@@ -1391,6 +1590,7 @@ export function App() {
1391
1590
  placeholder="Enter business name..."
1392
1591
  onSubmit={handleUpdateBusinessName}
1393
1592
  onCancel={() => setInputMode(null)}
1593
+ theme={theme}
1394
1594
  />
1395
1595
  )}
1396
1596
 
@@ -1402,6 +1602,7 @@ export function App() {
1402
1602
  placeholder="sk_live_..."
1403
1603
  onSubmit={handleUpdateStripeKey}
1404
1604
  onCancel={() => setInputMode(null)}
1605
+ theme={theme}
1405
1606
  />
1406
1607
  )}
1407
1608
 
@@ -1413,6 +1614,15 @@ export function App() {
1413
1614
  placeholder="America/New_York, Europe/London, auto..."
1414
1615
  onSubmit={handleUpdateTimezone}
1415
1616
  onCancel={() => setInputMode(null)}
1617
+ theme={theme}
1618
+ />
1619
+ )}
1620
+
1621
+ {inputMode === "select_theme" && (
1622
+ <ThemeSelectModal
1623
+ currentTheme={appSettings.theme}
1624
+ onSelect={handleSelectTheme}
1625
+ onCancel={() => setInputMode(null)}
1416
1626
  />
1417
1627
  )}
1418
1628
 
@@ -1421,6 +1631,7 @@ export function App() {
1421
1631
  projects={projectList.filter((p) => !p.archived)}
1422
1632
  onSelect={handleStartTimer}
1423
1633
  onCancel={() => setInputMode(null)}
1634
+ theme={theme}
1424
1635
  />
1425
1636
  )}
1426
1637
 
@@ -1431,6 +1642,7 @@ export function App() {
1431
1642
  duration={formatDuration(timerElapsed)}
1432
1643
  onSubmit={handleStopTimer}
1433
1644
  onCancel={() => setInputMode(null)}
1645
+ theme={theme}
1434
1646
  />
1435
1647
  )}
1436
1648
 
@@ -1446,6 +1658,7 @@ export function App() {
1446
1658
  setInputMode("edit_customer");
1447
1659
  }}
1448
1660
  onCancel={() => setInputMode(null)}
1661
+ theme={theme}
1449
1662
  />
1450
1663
  )}
1451
1664
 
@@ -1454,6 +1667,7 @@ export function App() {
1454
1667
  mode="create"
1455
1668
  onSubmit={handleCreateCustomer}
1456
1669
  onCancel={() => setInputMode("select_customer")}
1670
+ theme={theme}
1457
1671
  />
1458
1672
  )}
1459
1673
 
@@ -1468,6 +1682,7 @@ export function App() {
1468
1682
  setEditingCustomer(null);
1469
1683
  setInputMode("select_customer");
1470
1684
  }}
1685
+ theme={theme}
1471
1686
  />
1472
1687
  )}
1473
1688
 
@@ -1480,6 +1695,7 @@ export function App() {
1480
1695
  setInputMode(null);
1481
1696
  setEditingTimeEntry(null);
1482
1697
  }}
1698
+ theme={theme}
1483
1699
  />
1484
1700
  )}
1485
1701
 
@@ -1511,6 +1727,7 @@ export function App() {
1511
1727
  hasStripeKey={!!appSettings.stripeApiKey}
1512
1728
  onConfirm={handleCreateInvoice}
1513
1729
  onCancel={() => setInputMode(null)}
1730
+ theme={theme}
1514
1731
  />
1515
1732
  )}
1516
1733
 
@@ -1527,6 +1744,7 @@ export function App() {
1527
1744
  setConfirmAction(null);
1528
1745
  setConfirmMessage("");
1529
1746
  }}
1747
+ theme={theme}
1530
1748
  />
1531
1749
  )}
1532
1750
  </box>
@@ -1,7 +1,9 @@
1
1
  import { useKeyboard } from "@opentui/react";
2
2
  import {
3
+ CATPPUCCIN_MOCHA,
3
4
  type Customer,
4
5
  type TimeEntryWithProject,
6
+ type Theme,
5
7
  formatDateInTimezone,
6
8
  } from "../types.ts";
7
9
  import Modal from "./Modal.tsx";
@@ -16,6 +18,7 @@ interface CreateInvoiceModalProps {
16
18
  hasStripeKey: boolean;
17
19
  onConfirm: () => void;
18
20
  onCancel: () => void;
21
+ theme?: Theme;
19
22
  }
20
23
 
21
24
  function formatDuration(ms: number): string {
@@ -44,7 +47,10 @@ export function CreateInvoiceModal({
44
47
  hasStripeKey,
45
48
  onConfirm,
46
49
  onCancel,
50
+ theme = CATPPUCCIN_MOCHA,
47
51
  }: CreateInvoiceModalProps) {
52
+ const colors = theme.colors;
53
+
48
54
  useKeyboard((key) => {
49
55
  if (key.name === "escape") {
50
56
  onCancel();
@@ -97,20 +103,20 @@ export function CreateInvoiceModal({
97
103
  : baseHeight + lineItemsHeight;
98
104
 
99
105
  return (
100
- <Modal title="Create Stripe Invoice" height={Math.max(modalHeight, 20)}>
106
+ <Modal title="Create Stripe Invoice" height={Math.max(modalHeight, 20)} theme={theme}>
101
107
  {/* Customer info */}
102
108
  {customer && (
103
109
  <box style={{ flexDirection: "row", marginTop: 1, gap: 1 }}>
104
- <text fg="#8b5cf6" attributes="bold">
110
+ <text fg={colors.accentSecondary} attributes="bold">
105
111
  {customer.name}
106
112
  </text>
107
- <text fg="#64748b">{customer.email}</text>
113
+ <text fg={colors.textMuted}>{customer.email}</text>
108
114
  </box>
109
115
  )}
110
116
 
111
117
  {!customer && (
112
118
  <box style={{ marginTop: 1 }}>
113
- <text fg="#ef4444">
119
+ <text fg={colors.error}>
114
120
  This project has no customer linked. Please link a customer first
115
121
  using the 'c' key in the Projects view.
116
122
  </text>
@@ -119,7 +125,7 @@ export function CreateInvoiceModal({
119
125
 
120
126
  {!hasStripeKey && customer && (
121
127
  <box style={{ marginTop: 1 }}>
122
- <text fg="#ef4444">
128
+ <text fg={colors.error}>
123
129
  No Stripe API key configured. Add one in Settings to create
124
130
  invoices.
125
131
  </text>
@@ -129,22 +135,22 @@ export function CreateInvoiceModal({
129
135
  {customer && hasStripeKey && (
130
136
  <>
131
137
  {/* Line items header */}
132
- <text fg="#334155" style={{ marginTop: 1 }}>
138
+ <text fg={colors.borderSubtle} style={{ marginTop: 1 }}>
133
139
  {"─".repeat(56)}
134
140
  </text>
135
141
  <box style={{ flexDirection: "row", gap: 1 }}>
136
142
  <box style={{ width: 8 }}>
137
- <text fg="#94a3b8">Date</text>
143
+ <text fg={colors.accentSecondary}>Date</text>
138
144
  </box>
139
145
  <box style={{ flexGrow: 1 }}>
140
- <text fg="#94a3b8">Description</text>
146
+ <text fg={colors.accentSecondary}>Description</text>
141
147
  </box>
142
148
  <box style={{ width: 8 }}>
143
- <text fg="#94a3b8">Hours</text>
149
+ <text fg={colors.accentSecondary}>Hours</text>
144
150
  </box>
145
151
  {hourlyRate != null && (
146
152
  <box style={{ width: 10, alignItems: "flex-end" }}>
147
- <text fg="#94a3b8">Amount</text>
153
+ <text fg={colors.accentSecondary}>Amount</text>
148
154
  </box>
149
155
  )}
150
156
  </box>
@@ -154,21 +160,21 @@ export function CreateInvoiceModal({
154
160
  {lineItems.map((item, idx) => (
155
161
  <box key={idx} style={{ flexDirection: "row", gap: 1 }}>
156
162
  <box style={{ width: 8 }}>
157
- <text fg="#64748b">{item.date}</text>
163
+ <text fg={colors.textMuted}>{item.date}</text>
158
164
  </box>
159
165
  <box style={{ flexGrow: 1 }}>
160
- <text fg="#e2e8f0">
166
+ <text fg={colors.textPrimary}>
161
167
  {item.description.length > 30
162
168
  ? `${item.description.slice(0, 27)}...`
163
169
  : item.description}
164
170
  </text>
165
171
  </box>
166
172
  <box style={{ width: 8 }}>
167
- <text fg="#94a3b8">{item.hours}</text>
173
+ <text fg={colors.accentSecondary}>{item.hours}</text>
168
174
  </box>
169
175
  {hourlyRate != null && (
170
176
  <box style={{ width: 10, alignItems: "flex-end" }}>
171
- <text fg="#10b981">${item.amount.toFixed(2)}</text>
177
+ <text fg={colors.success}>${item.amount.toFixed(2)}</text>
172
178
  </box>
173
179
  )}
174
180
  </box>
@@ -176,26 +182,26 @@ export function CreateInvoiceModal({
176
182
  </scrollbox>
177
183
 
178
184
  {/* Totals */}
179
- <text fg="#334155">{"─".repeat(56)}</text>
185
+ <text fg={colors.borderSubtle}>{"─".repeat(56)}</text>
180
186
  <box style={{ flexDirection: "row", gap: 1 }}>
181
187
  <box style={{ width: 8 }}>
182
- <text fg="#ffffff" attributes="bold">
188
+ <text fg={colors.textPrimary} attributes="bold">
183
189
  Total
184
190
  </text>
185
191
  </box>
186
192
  <box style={{ flexGrow: 1 }}>
187
- <text fg="#94a3b8">
193
+ <text fg={colors.accentSecondary}>
188
194
  {entries.length} {entries.length === 1 ? "entry" : "entries"}
189
195
  </text>
190
196
  </box>
191
197
  <box style={{ width: 8 }}>
192
- <text fg="#ffffff" attributes="bold">
198
+ <text fg={colors.textPrimary} attributes="bold">
193
199
  {totalHours.toFixed(2)}
194
200
  </text>
195
201
  </box>
196
202
  {hourlyRate != null && (
197
203
  <box style={{ width: 10, alignItems: "flex-end" }}>
198
- <text fg="#10b981" attributes="bold">
204
+ <text fg={colors.success} attributes="bold">
199
205
  ${totalAmount.toFixed(2)}
200
206
  </text>
201
207
  </box>
@@ -204,7 +210,7 @@ export function CreateInvoiceModal({
204
210
 
205
211
  {hourlyRate != null && (
206
212
  <box style={{ marginTop: 1 }}>
207
- <text fg="#64748b">Rate: ${hourlyRate.toFixed(2)}/hr</text>
213
+ <text fg={colors.textMuted}>Rate: ${hourlyRate.toFixed(2)}/hr</text>
208
214
  </box>
209
215
  )}
210
216
  </>
@@ -212,7 +218,7 @@ export function CreateInvoiceModal({
212
218
 
213
219
  <box style={{ flexGrow: 1 }} />
214
220
 
215
- <text fg="#64748b">
221
+ <text fg={colors.textMuted}>
216
222
  {customer && hasStripeKey
217
223
  ? "Enter to create draft invoice, Esc to cancel"
218
224
  : "Esc to close"}