epoch-tui 0.1.0

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.
Files changed (147) hide show
  1. package/dist/App.d.ts +3 -0
  2. package/dist/App.js +38 -0
  3. package/dist/components/calendar/CalendarPane.d.ts +2 -0
  4. package/dist/components/calendar/CalendarPane.js +91 -0
  5. package/dist/components/calendar/DayCell.d.ts +7 -0
  6. package/dist/components/calendar/DayCell.js +26 -0
  7. package/dist/components/calendar/MonthView.d.ts +7 -0
  8. package/dist/components/calendar/MonthView.js +10 -0
  9. package/dist/components/common/BorderedBox.d.ts +18 -0
  10. package/dist/components/common/BorderedBox.js +29 -0
  11. package/dist/components/common/ClearTimelineDialog.d.ts +2 -0
  12. package/dist/components/common/ClearTimelineDialog.js +33 -0
  13. package/dist/components/common/FullscreenBackground.d.ts +11 -0
  14. package/dist/components/common/FullscreenBackground.js +12 -0
  15. package/dist/components/common/HelpDialog.d.ts +2 -0
  16. package/dist/components/common/HelpDialog.js +40 -0
  17. package/dist/components/common/Modal.d.ts +10 -0
  18. package/dist/components/common/Modal.js +15 -0
  19. package/dist/components/common/Separator.d.ts +8 -0
  20. package/dist/components/common/Separator.js +10 -0
  21. package/dist/components/common/ThemeDialog.d.ts +2 -0
  22. package/dist/components/common/ThemeDialog.js +117 -0
  23. package/dist/components/common/ThemedScreen.d.ts +22 -0
  24. package/dist/components/common/ThemedScreen.js +36 -0
  25. package/dist/components/common/ThemedText.d.ts +11 -0
  26. package/dist/components/common/ThemedText.js +20 -0
  27. package/dist/components/layout/Pane.d.ts +12 -0
  28. package/dist/components/layout/Pane.js +10 -0
  29. package/dist/components/layout/ThreeColumnLayout.d.ts +13 -0
  30. package/dist/components/layout/ThreeColumnLayout.js +22 -0
  31. package/dist/components/overview/OverviewScreen.d.ts +2 -0
  32. package/dist/components/overview/OverviewScreen.js +138 -0
  33. package/dist/components/tasks/TaskHeader.d.ts +7 -0
  34. package/dist/components/tasks/TaskHeader.js +8 -0
  35. package/dist/components/tasks/TaskItem.d.ts +10 -0
  36. package/dist/components/tasks/TaskItem.js +25 -0
  37. package/dist/components/tasks/TaskList.d.ts +11 -0
  38. package/dist/components/tasks/TaskList.js +11 -0
  39. package/dist/components/tasks/TasksPane.d.ts +2 -0
  40. package/dist/components/tasks/TasksPane.js +410 -0
  41. package/dist/components/timeline/TimelineEntry.d.ts +9 -0
  42. package/dist/components/timeline/TimelineEntry.js +26 -0
  43. package/dist/components/timeline/TimelinePane.d.ts +2 -0
  44. package/dist/components/timeline/TimelinePane.js +78 -0
  45. package/dist/contexts/AppContext.d.ts +47 -0
  46. package/dist/contexts/AppContext.js +104 -0
  47. package/dist/contexts/StorageContext.d.ts +15 -0
  48. package/dist/contexts/StorageContext.js +83 -0
  49. package/dist/contexts/ThemeContext.d.ts +15 -0
  50. package/dist/contexts/ThemeContext.js +44 -0
  51. package/dist/hooks/useKeyboardNav.d.ts +1 -0
  52. package/dist/hooks/useKeyboardNav.js +89 -0
  53. package/dist/hooks/useTerminalSize.d.ts +9 -0
  54. package/dist/hooks/useTerminalSize.js +34 -0
  55. package/dist/index.d.ts +2 -0
  56. package/dist/index.js +8 -0
  57. package/dist/services/calendarService.d.ts +18 -0
  58. package/dist/services/calendarService.js +57 -0
  59. package/dist/services/storage.d.ts +14 -0
  60. package/dist/services/storage.js +130 -0
  61. package/dist/services/taskService.d.ts +17 -0
  62. package/dist/services/taskService.js +106 -0
  63. package/dist/services/timelineService.d.ts +25 -0
  64. package/dist/services/timelineService.js +78 -0
  65. package/dist/themes/amazon.d.ts +2 -0
  66. package/dist/themes/amazon.js +37 -0
  67. package/dist/themes/amazonLight.d.ts +2 -0
  68. package/dist/themes/amazonLight.js +38 -0
  69. package/dist/themes/apple.d.ts +2 -0
  70. package/dist/themes/apple.js +38 -0
  71. package/dist/themes/appleLight.d.ts +2 -0
  72. package/dist/themes/appleLight.js +38 -0
  73. package/dist/themes/atomOneDark.d.ts +2 -0
  74. package/dist/themes/atomOneDark.js +37 -0
  75. package/dist/themes/atomOneLight.d.ts +2 -0
  76. package/dist/themes/atomOneLight.js +38 -0
  77. package/dist/themes/batman.d.ts +2 -0
  78. package/dist/themes/batman.js +37 -0
  79. package/dist/themes/catppuccin.d.ts +2 -0
  80. package/dist/themes/catppuccin.js +37 -0
  81. package/dist/themes/catppuccinLatte.d.ts +2 -0
  82. package/dist/themes/catppuccinLatte.js +38 -0
  83. package/dist/themes/claude.d.ts +2 -0
  84. package/dist/themes/claude.js +48 -0
  85. package/dist/themes/claudeCode.d.ts +2 -0
  86. package/dist/themes/claudeCode.js +47 -0
  87. package/dist/themes/cursor.d.ts +2 -0
  88. package/dist/themes/cursor.js +38 -0
  89. package/dist/themes/cursorLight.d.ts +2 -0
  90. package/dist/themes/cursorLight.js +38 -0
  91. package/dist/themes/dark.d.ts +2 -0
  92. package/dist/themes/dark.js +38 -0
  93. package/dist/themes/githubDark.d.ts +2 -0
  94. package/dist/themes/githubDark.js +37 -0
  95. package/dist/themes/githubLight.d.ts +2 -0
  96. package/dist/themes/githubLight.js +38 -0
  97. package/dist/themes/index.d.ts +9 -0
  98. package/dist/themes/index.js +83 -0
  99. package/dist/themes/instagram.d.ts +2 -0
  100. package/dist/themes/instagram.js +37 -0
  101. package/dist/themes/instagramLight.d.ts +2 -0
  102. package/dist/themes/instagramLight.js +38 -0
  103. package/dist/themes/intellij.d.ts +2 -0
  104. package/dist/themes/intellij.js +37 -0
  105. package/dist/themes/intellijLight.d.ts +2 -0
  106. package/dist/themes/intellijLight.js +38 -0
  107. package/dist/themes/light.d.ts +2 -0
  108. package/dist/themes/light.js +38 -0
  109. package/dist/themes/nord.d.ts +2 -0
  110. package/dist/themes/nord.js +37 -0
  111. package/dist/themes/nordLight.d.ts +2 -0
  112. package/dist/themes/nordLight.js +38 -0
  113. package/dist/themes/postman.d.ts +2 -0
  114. package/dist/themes/postman.js +37 -0
  115. package/dist/themes/postmanLight.d.ts +2 -0
  116. package/dist/themes/postmanLight.js +38 -0
  117. package/dist/themes/spiderman.d.ts +2 -0
  118. package/dist/themes/spiderman.js +37 -0
  119. package/dist/themes/terminal.d.ts +2 -0
  120. package/dist/themes/terminal.js +37 -0
  121. package/dist/themes/ubuntu.d.ts +2 -0
  122. package/dist/themes/ubuntu.js +37 -0
  123. package/dist/themes/ubuntuLight.d.ts +2 -0
  124. package/dist/themes/ubuntuLight.js +38 -0
  125. package/dist/themes/x.d.ts +2 -0
  126. package/dist/themes/x.js +38 -0
  127. package/dist/themes/xLight.d.ts +2 -0
  128. package/dist/themes/xLight.js +38 -0
  129. package/dist/types/calendar.d.ts +19 -0
  130. package/dist/types/calendar.js +1 -0
  131. package/dist/types/storage.d.ts +21 -0
  132. package/dist/types/storage.js +1 -0
  133. package/dist/types/task.d.ts +21 -0
  134. package/dist/types/task.js +1 -0
  135. package/dist/types/theme.d.ts +38 -0
  136. package/dist/types/theme.js +1 -0
  137. package/dist/types/timeline.d.ts +23 -0
  138. package/dist/types/timeline.js +9 -0
  139. package/dist/utils/date.d.ts +7 -0
  140. package/dist/utils/date.js +37 -0
  141. package/dist/utils/logger.d.ts +3 -0
  142. package/dist/utils/logger.js +39 -0
  143. package/dist/utils/tree.d.ts +11 -0
  144. package/dist/utils/tree.js +64 -0
  145. package/dist/utils/validation.d.ts +7 -0
  146. package/dist/utils/validation.js +35 -0
  147. package/package.json +44 -0
@@ -0,0 +1,410 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect, useMemo, useRef } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { TextInput } from "@inkjs/ui";
5
+ import { useTheme } from "../../contexts/ThemeContext";
6
+ import { useApp } from "../../contexts/AppContext";
7
+ import { Pane } from "../layout/Pane";
8
+ import { TaskHeader } from "./TaskHeader";
9
+ import { getDateString } from "../../utils/date";
10
+ import { taskService } from "../../services/taskService";
11
+ import { timelineService } from "../../services/timelineService";
12
+ import { TimelineEventType } from "../../types/timeline";
13
+ import { useTerminalSize } from "../../hooks/useTerminalSize";
14
+ export const TasksPane = () => {
15
+ const { tasks, setTasks, timeline, setTimeline, activePane, selectedDate, isInputMode, setIsInputMode, isModalOpen, } = useApp();
16
+ const { theme } = useTheme();
17
+ const [expandedIds, setExpandedIds] = useState(new Set());
18
+ const [selectedIndex, setSelectedIndex] = useState(0);
19
+ const [editMode, setEditMode] = useState("none");
20
+ const [editValue, setEditValue] = useState("");
21
+ const [parentTaskId, setParentTaskId] = useState(null);
22
+ const inputValueRef = useRef(""); // Track current input value for uncontrolled TextInput
23
+ const [inputKey, setInputKey] = useState(0); // Key to force TextInput remount
24
+ const [scrollOffset, setScrollOffset] = useState(0);
25
+ const { height: terminalHeight } = useTerminalSize();
26
+ const visibleRows = useMemo(() => {
27
+ // Header/Stats (4) + Keyboard Hints (3) + Padding/Margins (4) = ~11 lines
28
+ return Math.max(5, terminalHeight - 11);
29
+ }, [terminalHeight]);
30
+ const dateStr = getDateString(new Date(selectedDate.year, selectedDate.month, selectedDate.day));
31
+ const dayTasks = tasks[dateStr] || [];
32
+ const stats = taskService.getTaskStats(tasks, dateStr);
33
+ const isFocused = activePane === "tasks" && !isModalOpen;
34
+ // Flatten tasks for navigation (only visible ones based on expanded state)
35
+ const flatTasks = useMemo(() => {
36
+ const result = [];
37
+ const traverse = (taskList, depth) => {
38
+ for (const task of taskList) {
39
+ result.push({ task, depth });
40
+ if (task.children.length > 0 && expandedIds.has(task.id)) {
41
+ traverse(task.children, depth + 1);
42
+ }
43
+ }
44
+ };
45
+ traverse(dayTasks, 0);
46
+ return result;
47
+ }, [dayTasks, expandedIds]);
48
+ const selectedTask = flatTasks[selectedIndex]?.task;
49
+ const selectedTaskId = selectedTask?.id;
50
+ // Reset selection when day changes
51
+ useEffect(() => {
52
+ setSelectedIndex(0);
53
+ setEditMode("none");
54
+ }, [dateStr]);
55
+ // Clamp selection index when tasks change
56
+ useEffect(() => {
57
+ if (selectedIndex >= flatTasks.length && flatTasks.length > 0) {
58
+ setSelectedIndex(flatTasks.length - 1);
59
+ }
60
+ }, [flatTasks.length, selectedIndex]);
61
+ // Keep selected task in view
62
+ useEffect(() => {
63
+ if (selectedIndex < scrollOffset) {
64
+ setScrollOffset(selectedIndex);
65
+ }
66
+ else if (selectedIndex >= scrollOffset + visibleRows) {
67
+ setScrollOffset(selectedIndex - visibleRows + 1);
68
+ }
69
+ }, [selectedIndex, visibleRows, scrollOffset]);
70
+ // Reset scroll when day changes
71
+ useEffect(() => {
72
+ setScrollOffset(0);
73
+ }, [dateStr]);
74
+ const handleAddTask = () => {
75
+ inputValueRef.current = "";
76
+ setEditMode("add");
77
+ setEditValue("");
78
+ setInputKey((k) => k + 1);
79
+ setIsInputMode(true);
80
+ };
81
+ const handleEditTask = () => {
82
+ if (selectedTask) {
83
+ inputValueRef.current = selectedTask.title;
84
+ setEditMode("edit");
85
+ setEditValue(selectedTask.title);
86
+ setInputKey((k) => k + 1);
87
+ setIsInputMode(true);
88
+ }
89
+ };
90
+ const handleAddSubtask = () => {
91
+ if (selectedTask) {
92
+ inputValueRef.current = "";
93
+ setEditMode("addSubtask");
94
+ setEditValue("");
95
+ setParentTaskId(selectedTask.id);
96
+ setInputKey((k) => k + 1);
97
+ setIsInputMode(true);
98
+ setExpandedIds((prev) => new Set(prev).add(selectedTask.id));
99
+ }
100
+ };
101
+ const handleDeleteTask = () => {
102
+ if (selectedTaskId) {
103
+ try {
104
+ const updated = taskService.deleteTask(tasks, selectedTaskId);
105
+ setTasks(updated);
106
+ // Remove all timeline events for this task
107
+ const updatedTimeline = timelineService.removeEventsByTaskId(timeline, selectedTaskId);
108
+ setTimeline(updatedTimeline);
109
+ }
110
+ catch (err) {
111
+ console.error("Error deleting task:", err);
112
+ }
113
+ }
114
+ };
115
+ const handleChangeState = (newState) => {
116
+ if (selectedTaskId && selectedTask) {
117
+ try {
118
+ const previousState = selectedTask.state;
119
+ const updated = taskService.changeTaskState(tasks, selectedTaskId, newState);
120
+ setTasks(updated);
121
+ // Handle timeline based on state change
122
+ if (newState === "todo") {
123
+ // If toggling back to todo, remove the previous state event
124
+ const eventTypeToRemove = {
125
+ todo: TimelineEventType.STARTED, // shouldn't happen
126
+ completed: TimelineEventType.COMPLETED,
127
+ delegated: TimelineEventType.DELEGATED,
128
+ delayed: TimelineEventType.DELAYED,
129
+ };
130
+ const updatedTimeline = timelineService.removeLastEventByType(timeline, selectedTaskId, eventTypeToRemove[previousState]);
131
+ setTimeline(updatedTimeline);
132
+ }
133
+ else {
134
+ // Create timeline event for state change
135
+ const eventTypeMap = {
136
+ todo: TimelineEventType.STARTED, // shouldn't happen
137
+ completed: TimelineEventType.COMPLETED,
138
+ delegated: TimelineEventType.DELEGATED,
139
+ delayed: TimelineEventType.DELAYED,
140
+ };
141
+ const event = timelineService.createEvent(selectedTaskId, selectedTask.title, eventTypeMap[newState], new Date(), previousState, newState);
142
+ const updatedTimeline = timelineService.addEvent(timeline, event);
143
+ setTimeline(updatedTimeline);
144
+ }
145
+ }
146
+ catch (err) {
147
+ console.error("Error changing task state:", err);
148
+ }
149
+ }
150
+ };
151
+ const handleToggleComplete = () => {
152
+ if (selectedTask) {
153
+ const newState = selectedTask.state === "completed" ? "todo" : "completed";
154
+ handleChangeState(newState);
155
+ }
156
+ };
157
+ const handleSubmitEdit = (value) => {
158
+ const trimmed = value.trim();
159
+ if (!trimmed) {
160
+ setEditMode("none");
161
+ setEditValue("");
162
+ setParentTaskId(null);
163
+ setIsInputMode(false);
164
+ return;
165
+ }
166
+ try {
167
+ if (editMode === "add") {
168
+ const newTask = taskService.createTask(trimmed, dateStr);
169
+ const newTasks = {
170
+ ...tasks,
171
+ [dateStr]: [...dayTasks, newTask],
172
+ };
173
+ setTasks(newTasks);
174
+ setSelectedIndex(flatTasks.length); // Select newly added task
175
+ // No timeline event for task creation - only track started/completed/etc
176
+ }
177
+ else if (editMode === "addSubtask" && parentTaskId) {
178
+ const updated = taskService.addSubtask(tasks, parentTaskId, trimmed);
179
+ setTasks(updated);
180
+ // Find and select the newly added subtask
181
+ const parentIndex = flatTasks.findIndex((ft) => ft.task.id === parentTaskId);
182
+ if (parentIndex !== -1) {
183
+ setSelectedIndex(parentIndex + 1); // Select first child (newly added)
184
+ }
185
+ // No timeline event for subtask creation
186
+ }
187
+ else if (editMode === "edit" && selectedTaskId) {
188
+ const updated = taskService.updateTask(tasks, selectedTaskId, {
189
+ title: trimmed,
190
+ });
191
+ setTasks(updated);
192
+ }
193
+ }
194
+ catch (err) {
195
+ console.error("Error saving task:", err);
196
+ }
197
+ setEditMode("none");
198
+ setEditValue("");
199
+ setParentTaskId(null);
200
+ setIsInputMode(false);
201
+ };
202
+ const handleCancelEdit = () => {
203
+ setEditMode("none");
204
+ setEditValue("");
205
+ setParentTaskId(null);
206
+ setIsInputMode(false);
207
+ };
208
+ const handleToggleExpand = () => {
209
+ if (selectedTaskId && selectedTask?.children.length > 0) {
210
+ setExpandedIds((prev) => {
211
+ const newSet = new Set(prev);
212
+ if (newSet.has(selectedTaskId)) {
213
+ newSet.delete(selectedTaskId);
214
+ }
215
+ else {
216
+ newSet.add(selectedTaskId);
217
+ }
218
+ return newSet;
219
+ });
220
+ }
221
+ };
222
+ const handleExpand = () => {
223
+ if (selectedTaskId && selectedTask?.children.length > 0) {
224
+ setExpandedIds((prev) => new Set(prev).add(selectedTaskId));
225
+ }
226
+ };
227
+ const handleCollapse = () => {
228
+ if (selectedTaskId && selectedTask?.children.length > 0) {
229
+ setExpandedIds((prev) => {
230
+ const newSet = new Set(prev);
231
+ newSet.delete(selectedTaskId);
232
+ return newSet;
233
+ });
234
+ }
235
+ };
236
+ const handleExpandAll = () => {
237
+ // Get all task IDs that have children
238
+ const allParentIds = new Set();
239
+ const collectParents = (taskList) => {
240
+ for (const task of taskList) {
241
+ if (task.children.length > 0) {
242
+ allParentIds.add(task.id);
243
+ collectParents(task.children);
244
+ }
245
+ }
246
+ };
247
+ collectParents(dayTasks);
248
+ setExpandedIds(allParentIds);
249
+ };
250
+ const handleCollapseAll = () => {
251
+ setExpandedIds(new Set());
252
+ };
253
+ // Handle escape key to cancel editing
254
+ useInput((_input, key) => {
255
+ if (key.escape) {
256
+ handleCancelEdit();
257
+ }
258
+ }, { isActive: isFocused && editMode !== "none" });
259
+ // Input handler for navigation/command mode
260
+ useInput((input, key) => {
261
+ // Expand/Collapse ALL with Cmd/Ctrl + arrow keys
262
+ // Note: On Mac terminals, Cmd+Left sends Ctrl+a and Cmd+Right sends Ctrl+e
263
+ if ((key.meta || key.ctrl) && (input === "a" || key.leftArrow)) {
264
+ handleCollapseAll();
265
+ return;
266
+ }
267
+ if ((key.meta || key.ctrl) && (input === "e" || key.rightArrow)) {
268
+ handleExpandAll();
269
+ return;
270
+ }
271
+ // Navigation
272
+ if (input === "j" || key.downArrow) {
273
+ setSelectedIndex((prev) => Math.min(prev + 1, flatTasks.length - 1));
274
+ return;
275
+ }
276
+ if (input === "k" || key.upArrow) {
277
+ setSelectedIndex((prev) => Math.max(prev - 1, 0));
278
+ return;
279
+ }
280
+ // Expand/Collapse current with arrow keys (no modifiers)
281
+ if (key.leftArrow && !key.meta && !key.ctrl) {
282
+ handleCollapse();
283
+ return;
284
+ }
285
+ if (key.rightArrow && !key.meta && !key.ctrl) {
286
+ handleExpand();
287
+ return;
288
+ }
289
+ // Task actions (only if no modifier keys are pressed)
290
+ if (input === "a" && !key.meta && !key.ctrl && !key.shift) {
291
+ handleAddTask();
292
+ return;
293
+ }
294
+ if (input === "e" &&
295
+ selectedTask &&
296
+ !key.meta &&
297
+ !key.ctrl &&
298
+ !key.shift) {
299
+ handleEditTask();
300
+ return;
301
+ }
302
+ if (input === "d" && selectedTask) {
303
+ handleDeleteTask();
304
+ return;
305
+ }
306
+ if (input === " " && selectedTask) {
307
+ handleToggleComplete();
308
+ return;
309
+ }
310
+ if (input === "D" && selectedTask) {
311
+ handleChangeState("delegated");
312
+ return;
313
+ }
314
+ if (input === "x" && selectedTask) {
315
+ // Toggle delayed state
316
+ if (selectedTask.state === "delayed") {
317
+ handleChangeState("todo");
318
+ }
319
+ else {
320
+ handleChangeState("delayed");
321
+ }
322
+ return;
323
+ }
324
+ if (input === "s" && selectedTask) {
325
+ try {
326
+ // Toggle start - if already started (has startTime but no endTime), unstart it
327
+ if (selectedTask.startTime && !selectedTask.endTime) {
328
+ // Unstart: clear startTime and remove timeline event
329
+ const updated = taskService.updateTask(tasks, selectedTaskId, {
330
+ startTime: undefined,
331
+ });
332
+ setTasks(updated);
333
+ // Remove the started event from timeline
334
+ const updatedTimeline = timelineService.removeLastEventByType(timeline, selectedTaskId, TimelineEventType.STARTED);
335
+ setTimeline(updatedTimeline);
336
+ }
337
+ else {
338
+ // Start task
339
+ const updated = taskService.startTask(tasks, selectedTaskId);
340
+ setTasks(updated);
341
+ // Create timeline event for starting task
342
+ const event = timelineService.createEvent(selectedTaskId, selectedTask.title, TimelineEventType.STARTED, new Date());
343
+ const updatedTimeline = timelineService.addEvent(timeline, event);
344
+ setTimeline(updatedTimeline);
345
+ }
346
+ }
347
+ catch (err) {
348
+ console.error("Error toggling task start:", err);
349
+ }
350
+ return;
351
+ }
352
+ if (key.return && selectedTask) {
353
+ handleToggleExpand();
354
+ return;
355
+ }
356
+ if (key.tab && selectedTask) {
357
+ handleAddSubtask();
358
+ return;
359
+ }
360
+ }, { isActive: isFocused && editMode === "none" });
361
+ return (_jsx(Pane, { title: "Tasks", isFocused: isFocused, children: _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(TaskHeader, { selectedDate: new Date(selectedDate.year, selectedDate.month, selectedDate.day), completionPercentage: stats.percentage }), editMode === "add" && (_jsxs(Box, { marginY: 1, children: [_jsx(Text, { color: theme.colors.focusIndicator, children: "> " }), _jsx(TextInput, { defaultValue: "", placeholder: "Enter task name...", onChange: (val) => {
362
+ inputValueRef.current = val;
363
+ }, onSubmit: handleSubmitEdit }, `add-${inputKey}`)] })), editMode === "addSubtask" && (_jsxs(Box, { marginY: 1, children: [_jsx(Text, { color: theme.colors.focusIndicator, children: "> " }), _jsx(Text, { color: theme.colors.keyboardHint, children: " " }), _jsx(TextInput, { defaultValue: "", placeholder: "Enter subtask name...", onChange: (val) => {
364
+ inputValueRef.current = val;
365
+ }, onSubmit: handleSubmitEdit }, `subtask-${inputKey}`)] })), dayTasks.length === 0 &&
366
+ editMode !== "add" &&
367
+ editMode !== "addSubtask" ? (_jsx(Box, { marginY: 1, children: _jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: "No tasks. Press 'a' to add one." }) })) : (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingRight: 2, children: [scrollOffset > 0 && (_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: "-- more above --" }) })), flatTasks
368
+ .slice(scrollOffset, scrollOffset + visibleRows)
369
+ .map(({ task, depth }, sliceIndex) => {
370
+ const index = scrollOffset + sliceIndex;
371
+ const isSelected = index === selectedIndex;
372
+ const isExpanded = expandedIds.has(task.id);
373
+ const isEditing = editMode === "edit" && isSelected;
374
+ if (isEditing) {
375
+ return (_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.focusIndicator, children: "> " }), _jsx(TextInput, { defaultValue: editValue, onChange: (val) => {
376
+ inputValueRef.current = val;
377
+ }, onSubmit: handleSubmitEdit }, `edit-${inputKey}`)] }, task.id));
378
+ }
379
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected
380
+ ? theme.colors.focusIndicator
381
+ : theme.colors.foreground, children: isSelected ? ">" : " " }), _jsx(Text, { children: " " }), _jsx(Text, { children: " ".repeat(depth) }), _jsx(Text, { color: isSelected
382
+ ? theme.colors.focusIndicator
383
+ : getStateColor(task.state, theme), children: getCheckbox(task.state) }), _jsx(Text, { children: " " }), task.children.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { children: isExpanded ? "▼" : "▶" }), _jsx(Text, { children: " " })] })), _jsx(Text, { color: isSelected
384
+ ? theme.colors.focusIndicator
385
+ : getStateColor(task.state, theme), strikethrough: task.state === "completed", dimColor: task.state === "delayed" && !isSelected, children: task.title }), task.startTime && !task.endTime && (_jsxs(Text, { color: isSelected
386
+ ? theme.colors.focusIndicator
387
+ : theme.colors.timelineEventStarted, children: [" ", "\u25B6"] }))] }, task.id));
388
+ }), scrollOffset + visibleRows < flatTasks.length && (_jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: "-- more below --" }) }))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: "j/k: navigate a: add Tab: add subtask e: edit d: delete" }), _jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: "Space: complete D: delegate x: delay s: start" }), _jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: "\u2190/\u2192: collapse/expand Cmd+\u2190/\u2192: collapse/expand all" })] })] }) }));
389
+ };
390
+ function getCheckbox(state) {
391
+ switch (state) {
392
+ case "completed":
393
+ return "[✓]";
394
+ case "delegated":
395
+ return "[→]";
396
+ case "delayed":
397
+ return "[‖]";
398
+ default:
399
+ return "[ ]";
400
+ }
401
+ }
402
+ function getStateColor(state, theme) {
403
+ const colors = {
404
+ todo: theme.colors.taskStateTodo,
405
+ completed: theme.colors.taskStateCompleted,
406
+ delegated: theme.colors.taskStateDelegated,
407
+ delayed: theme.colors.taskStateDelayed,
408
+ };
409
+ return colors[state] || theme.colors.foreground;
410
+ }
@@ -0,0 +1,9 @@
1
+ import React from "react";
2
+ import type { TimelineEvent } from "../../types/timeline";
3
+ interface TimelineEntryProps {
4
+ event: TimelineEvent;
5
+ isLast: boolean;
6
+ hasNextSameTask: boolean;
7
+ }
8
+ export declare const TimelineEntry: React.FC<TimelineEntryProps>;
9
+ export {};
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { useTheme } from "../../contexts/ThemeContext";
4
+ export const TimelineEntry = ({ event, isLast, hasNextSameTask, }) => {
5
+ const { theme } = useTheme();
6
+ // Format time as "7:31 am"
7
+ const hours = event.timestamp.getHours();
8
+ const minutes = event.timestamp.getMinutes();
9
+ const ampm = hours >= 12 ? "pm" : "am";
10
+ const hour12 = hours % 12 || 12;
11
+ const timeStr = `${hour12}:${minutes.toString().padStart(2, "0")} ${ampm}`;
12
+ const typeStr = event.type.charAt(0).toUpperCase() + event.type.slice(1);
13
+ // Color mapping for event types
14
+ const eventTypeColors = {
15
+ created: theme.colors.foreground,
16
+ started: theme.colors.foreground,
17
+ completed: theme.colors.taskStateCompleted,
18
+ delegated: theme.colors.taskStateDelegated,
19
+ delayed: theme.colors.taskStateDelayed,
20
+ updated: theme.colors.foreground,
21
+ };
22
+ const color = eventTypeColors[event.type] || theme.colors.foreground;
23
+ // Circle style: hollow for started, filled for completed/delegated/delayed
24
+ const isFilledCircle = ["completed", "delegated", "delayed"].includes(event.type);
25
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Box, { width: 3, justifyContent: "center", children: _jsx(Text, { color: color, children: isFilledCircle ? "●" : "○" }) }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Box, { children: _jsxs(Text, { children: [_jsxs(Text, { color: color, bold: true, children: [typeStr, ":"] }), _jsxs(Text, { color: theme.colors.foreground, children: [" ", event.taskTitle] })] }) }), _jsx(Box, { children: _jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: timeStr }) })] })] }), !isLast && (_jsx(Box, { children: _jsx(Box, { width: 3, justifyContent: "center", children: _jsx(Text, { color: hasNextSameTask ? color : theme.colors.border, children: "\u2502" }) }) }))] }));
26
+ };
@@ -0,0 +1,2 @@
1
+ import React from "react";
2
+ export declare const TimelinePane: React.FC;
@@ -0,0 +1,78 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect, useMemo } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { useTheme } from "../../contexts/ThemeContext";
5
+ import { useApp } from "../../contexts/AppContext";
6
+ import { Pane } from "../layout/Pane";
7
+ import { TimelineEntry } from "./TimelineEntry";
8
+ import { getDateString } from "../../utils/date";
9
+ import { useTerminalSize } from "../../hooks/useTerminalSize";
10
+ export const TimelinePane = () => {
11
+ const { theme } = useTheme();
12
+ const { selectedDate, timeline, activePane, isModalOpen, isInputMode } = useApp();
13
+ const isFocused = activePane === "timeline" && !isModalOpen;
14
+ const [scrollOffset, setScrollOffset] = useState(0);
15
+ const { height: terminalHeight } = useTerminalSize();
16
+ const visibleRows = useMemo(() => {
17
+ // Pane header/footer and padding takes some space
18
+ return Math.max(5, terminalHeight - 10);
19
+ }, [terminalHeight]);
20
+ const dateStr = getDateString(new Date(selectedDate.year, selectedDate.month, selectedDate.day));
21
+ const dayEvents = timeline[dateStr] || [];
22
+ // Group events by task, then sort by timestamp within each task group
23
+ const sortedEvents = useMemo(() => {
24
+ // First, group events by taskId
25
+ const eventsByTask = new Map();
26
+ dayEvents.forEach((event) => {
27
+ const taskEvents = eventsByTask.get(event.taskId) || [];
28
+ taskEvents.push(event);
29
+ eventsByTask.set(event.taskId, taskEvents);
30
+ });
31
+ // Sort events within each task group by timestamp
32
+ eventsByTask.forEach((events) => {
33
+ events.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
34
+ });
35
+ // Convert to array of task groups and sort by earliest event timestamp
36
+ const taskGroups = Array.from(eventsByTask.entries()).sort(([, eventsA], [, eventsB]) => {
37
+ const earliestA = eventsA[0]?.timestamp.getTime() || 0;
38
+ const earliestB = eventsB[0]?.timestamp.getTime() || 0;
39
+ return earliestA - earliestB;
40
+ });
41
+ // Flatten back to a single array
42
+ return taskGroups.flatMap(([, events]) => events);
43
+ }, [dayEvents]);
44
+ // Reset scroll when date changes
45
+ useEffect(() => {
46
+ setScrollOffset(0);
47
+ }, [dateStr]);
48
+ // Scroll with j/k when focused
49
+ useInput((input, key) => {
50
+ if (!isFocused)
51
+ return;
52
+ if (input === "j" || key.downArrow) {
53
+ setScrollOffset((prev) => Math.min(prev + 1, Math.max(0, sortedEvents.length - visibleRows)));
54
+ }
55
+ if (input === "k" || key.upArrow) {
56
+ setScrollOffset((prev) => Math.max(prev - 1, 0));
57
+ }
58
+ // Page down/up
59
+ if (input === "d" && key.ctrl) {
60
+ setScrollOffset((prev) => Math.min(prev + Math.floor(visibleRows / 2), Math.max(0, sortedEvents.length - visibleRows)));
61
+ }
62
+ if (input === "u" && key.ctrl) {
63
+ setScrollOffset((prev) => Math.max(prev - Math.floor(visibleRows / 2), 0));
64
+ }
65
+ }, { isActive: isFocused && !isInputMode });
66
+ // Get visible events based on scroll
67
+ const visibleEvents = sortedEvents.slice(scrollOffset, scrollOffset + visibleRows);
68
+ // Check if scrolling is possible
69
+ const canScrollUp = scrollOffset > 0;
70
+ const canScrollDown = scrollOffset + visibleRows < sortedEvents.length;
71
+ return (_jsx(Pane, { title: "Timeline", isFocused: isFocused, children: _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [sortedEvents.length === 0 ? (_jsxs(Box, { marginY: 1, flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: "No activities yet." }), _jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: "Press 's' to start a task." })] })) : (_jsxs(Box, { flexDirection: "column", children: [canScrollUp && (_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: "-- more above --" }) })), _jsx(Box, { flexDirection: "column", children: visibleEvents.map((event, index) => {
72
+ const globalIndex = scrollOffset + index;
73
+ const isLast = globalIndex === sortedEvents.length - 1;
74
+ const nextEvent = sortedEvents[globalIndex + 1];
75
+ const hasNextSameTask = nextEvent && nextEvent.taskId === event.taskId;
76
+ return (_jsx(TimelineEntry, { event: event, isLast: isLast, hasNextSameTask: hasNextSameTask }, event.id));
77
+ }) }), canScrollDown && (_jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: "-- more below --" }) }))] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: "j/k: scroll" }) })] }) }));
78
+ };
@@ -0,0 +1,47 @@
1
+ import React from "react";
2
+ import type { TaskTree } from "../types/task";
3
+ import type { TimelineEvent } from "../types/timeline";
4
+ import type { CalendarDate } from "../types/calendar";
5
+ interface AppContextType {
6
+ selectedDate: CalendarDate;
7
+ setSelectedDate: (date: CalendarDate) => void;
8
+ tasks: TaskTree;
9
+ setTasks: (tasks: TaskTree) => void;
10
+ timeline: {
11
+ [date: string]: TimelineEvent[];
12
+ };
13
+ setTimeline: (timeline: {
14
+ [date: string]: TimelineEvent[];
15
+ }) => void;
16
+ activePane: "calendar" | "tasks" | "timeline";
17
+ setActivePane: (pane: "calendar" | "tasks" | "timeline") => void;
18
+ showHelp: boolean;
19
+ setShowHelp: (show: boolean) => void;
20
+ isInputMode: boolean;
21
+ setIsInputMode: (mode: boolean) => void;
22
+ showOverview: boolean;
23
+ setShowOverview: (show: boolean) => void;
24
+ overviewMonth: {
25
+ year: number;
26
+ month: number;
27
+ };
28
+ setOverviewMonth: (date: {
29
+ year: number;
30
+ month: number;
31
+ }) => void;
32
+ exitConfirmation: boolean;
33
+ setExitConfirmation: (show: boolean) => void;
34
+ showThemeDialog: boolean;
35
+ setShowThemeDialog: (show: boolean) => void;
36
+ showClearTimelineDialog: boolean;
37
+ setShowClearTimelineDialog: (show: boolean) => void;
38
+ clearTimelineForDate: (dateStr: string) => void;
39
+ isModalOpen: boolean;
40
+ saveNow: () => Promise<void>;
41
+ }
42
+ interface AppProviderProps {
43
+ children: React.ReactNode;
44
+ }
45
+ export declare const AppProvider: React.FC<AppProviderProps>;
46
+ export declare const useApp: () => AppContextType;
47
+ export {};