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
package/dist/App.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ declare const App: React.FC;
3
+ export default App;
package/dist/App.js ADDED
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { ThemeProvider, useTheme } from "./contexts/ThemeContext";
4
+ import { StorageProvider } from "./contexts/StorageContext";
5
+ import { AppProvider, useApp } from "./contexts/AppContext";
6
+ import { useKeyboardNav } from "./hooks/useKeyboardNav";
7
+ import { useTerminalSize } from "./hooks/useTerminalSize";
8
+ import { ThreeColumnLayout } from "./components/layout/ThreeColumnLayout";
9
+ import { CalendarPane } from "./components/calendar/CalendarPane";
10
+ import { TasksPane } from "./components/tasks/TasksPane";
11
+ import { TimelinePane } from "./components/timeline/TimelinePane";
12
+ import { HelpDialog } from "./components/common/HelpDialog";
13
+ import { ThemeDialog } from "./components/common/ThemeDialog";
14
+ import { ClearTimelineDialog } from "./components/common/ClearTimelineDialog";
15
+ import { OverviewScreen } from "./components/overview/OverviewScreen";
16
+ import { FullscreenBackground } from "./components/common/FullscreenBackground";
17
+ const AppContent = () => {
18
+ const { showHelp, showOverview, exitConfirmation, activePane, showThemeDialog, showClearTimelineDialog, } = useApp();
19
+ const { theme } = useTheme();
20
+ useKeyboardNav();
21
+ /* Use available width and height - automatically updates on resize */
22
+ const { width, height } = useTerminalSize();
23
+ // When a modal dialog is open, render only the modal
24
+ if (showThemeDialog) {
25
+ return _jsx(ThemeDialog, {});
26
+ }
27
+ if (showHelp) {
28
+ return _jsx(HelpDialog, {});
29
+ }
30
+ if (showClearTimelineDialog) {
31
+ return _jsx(ClearTimelineDialog, {});
32
+ }
33
+ return (_jsx(FullscreenBackground, { backgroundColor: theme.colors.background || "black", children: _jsxs(Box, { flexDirection: "column", width: width, height: height, padding: 1, backgroundColor: theme.colors.background, children: [showOverview ? (_jsx(OverviewScreen, {})) : (_jsx(ThreeColumnLayout, { leftPane: _jsx(CalendarPane, {}), centerPane: _jsx(TasksPane, {}), rightPane: _jsx(TimelinePane, {}), activePane: activePane, height: height - 2 })), exitConfirmation && (_jsx(Box, { width: "100%", justifyContent: "center", paddingY: 1, children: _jsxs(Text, { backgroundColor: "red", color: "white", bold: true, children: [" ", "Press Ctrl+C again to exit Epoch", " "] }) }))] }) }));
34
+ };
35
+ const App = () => {
36
+ return (_jsx(StorageProvider, { children: _jsx(ThemeProvider, { initialTheme: "dark", children: _jsx(AppProvider, { children: _jsx(AppContent, {}) }) }) }));
37
+ };
38
+ export default App;
@@ -0,0 +1,2 @@
1
+ import React from "react";
2
+ export declare const CalendarPane: React.FC;
@@ -0,0 +1,91 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { addDays, addWeeks, subDays, subWeeks } from "date-fns";
5
+ import { useTheme } from "../../contexts/ThemeContext";
6
+ import { useApp } from "../../contexts/AppContext";
7
+ import { Pane } from "../layout/Pane";
8
+ import { MonthView } from "./MonthView";
9
+ import { calendarService } from "../../services/calendarService";
10
+ export const CalendarPane = () => {
11
+ const { theme } = useTheme();
12
+ const { selectedDate, setSelectedDate, tasks, activePane, isModalOpen, isInputMode } = useApp();
13
+ const [currentMonth, setCurrentMonth] = useState({
14
+ year: selectedDate.year,
15
+ month: selectedDate.month,
16
+ });
17
+ // Update currentMonth when selectedDate changes to different month
18
+ useEffect(() => {
19
+ if (selectedDate.month !== currentMonth.month ||
20
+ selectedDate.year !== currentMonth.year) {
21
+ setCurrentMonth({ year: selectedDate.year, month: selectedDate.month });
22
+ }
23
+ }, [selectedDate.year, selectedDate.month]);
24
+ const calendarView = calendarService.generateMonthView(currentMonth.year, currentMonth.month, selectedDate, tasks);
25
+ const isFocused = activePane === "calendar" && !isModalOpen;
26
+ const navigateDate = (newDate) => {
27
+ setSelectedDate({
28
+ year: newDate.getFullYear(),
29
+ month: newDate.getMonth(),
30
+ day: newDate.getDate(),
31
+ });
32
+ };
33
+ useInput((input, key) => {
34
+ if (!isFocused)
35
+ return;
36
+ const currentDate = new Date(selectedDate.year, selectedDate.month, selectedDate.day);
37
+ // Navigate by day (h/l or left/right)
38
+ if (input === "h" || key.leftArrow) {
39
+ navigateDate(subDays(currentDate, 1));
40
+ return;
41
+ }
42
+ if (input === "l" || key.rightArrow) {
43
+ navigateDate(addDays(currentDate, 1));
44
+ return;
45
+ }
46
+ // Navigate by week (j/k or down/up)
47
+ if (input === "j" || key.downArrow) {
48
+ navigateDate(addWeeks(currentDate, 1));
49
+ return;
50
+ }
51
+ if (input === "k" || key.upArrow) {
52
+ navigateDate(subWeeks(currentDate, 1));
53
+ return;
54
+ }
55
+ // Navigate by month
56
+ if (input === "n") {
57
+ const next = calendarService.getNextMonth(currentMonth.year, currentMonth.month);
58
+ setCurrentMonth(next);
59
+ // Move selected date to first day of new month
60
+ setSelectedDate({
61
+ year: next.year,
62
+ month: next.month,
63
+ day: 1,
64
+ });
65
+ return;
66
+ }
67
+ if (input === "p") {
68
+ const prev = calendarService.getPreviousMonth(currentMonth.year, currentMonth.month);
69
+ setCurrentMonth(prev);
70
+ // Move selected date to first day of new month
71
+ setSelectedDate({
72
+ year: prev.year,
73
+ month: prev.month,
74
+ day: 1,
75
+ });
76
+ return;
77
+ }
78
+ // Go to today
79
+ if (input === "T") {
80
+ const today = new Date();
81
+ setSelectedDate({
82
+ year: today.getFullYear(),
83
+ month: today.getMonth(),
84
+ day: today.getDate(),
85
+ });
86
+ return;
87
+ }
88
+ }, { isActive: isFocused && !isInputMode });
89
+ const monthName = calendarService.getMonthName(currentMonth.month);
90
+ return (_jsx(Pane, { title: `${monthName} ${currentMonth.year}`, isFocused: isFocused, center: true, children: _jsxs(Box, { flexDirection: "column", alignItems: "center", children: [_jsx(MonthView, { calendarView: calendarView }), _jsxs(Box, { marginTop: 1, flexDirection: "column", alignItems: "center", children: [_jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: "h/l: days j/k: weeks" }), _jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: "n/p: month T: today" })] })] }) }));
91
+ };
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ import type { CalendarDay } from "../../types/calendar";
3
+ interface DayCellProps {
4
+ day: CalendarDay;
5
+ }
6
+ export declare const DayCell: React.FC<DayCellProps>;
7
+ export {};
@@ -0,0 +1,26 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box } from "ink";
3
+ import { Text } from "../common/ThemedText";
4
+ import { useTheme } from "../../contexts/ThemeContext";
5
+ export const DayCell = ({ day }) => {
6
+ const { theme } = useTheme();
7
+ let textColor = theme.colors.calendarDayOtherMonth;
8
+ if (day.isCurrentMonth) {
9
+ textColor = day.hasTasks
10
+ ? theme.colors.calendarDayWithTasks
11
+ : theme.colors.foreground;
12
+ }
13
+ // Determine indicators - selected takes priority, then today
14
+ const isSelected = day.isSelected;
15
+ const isToday = day.isToday && !day.isSelected;
16
+ if (isSelected) {
17
+ textColor = theme.colors.calendarSelected;
18
+ }
19
+ else if (day.isToday) {
20
+ textColor = theme.colors.calendarToday;
21
+ }
22
+ const dayNum = day.date.day.toString().padStart(2, " ");
23
+ const leftBracket = isSelected ? "[" : isToday ? "(" : " ";
24
+ const rightBracket = isSelected ? "]" : isToday ? ")" : " ";
25
+ return (_jsx(Box, { width: 4, children: _jsxs(Text, { color: textColor, children: [leftBracket, dayNum, rightBracket] }) }));
26
+ };
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ import type { CalendarView } from "../../types/calendar";
3
+ interface MonthViewProps {
4
+ calendarView: CalendarView;
5
+ }
6
+ export declare const MonthView: React.FC<MonthViewProps>;
7
+ export {};
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box } from "ink";
3
+ import { Text } from "../common/ThemedText";
4
+ import { useTheme } from "../../contexts/ThemeContext";
5
+ import { DayCell } from "./DayCell";
6
+ export const MonthView = ({ calendarView }) => {
7
+ const { theme } = useTheme();
8
+ const dayHeaders = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
9
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: dayHeaders.map((day) => (_jsx(Box, { width: 4, children: _jsx(Text, { color: theme.colors.calendarHeader, bold: true, children: day }) }, day))) }), calendarView.weeks.map((week, weekIdx) => (_jsx(Box, { marginBottom: 0, children: week.map((day) => (_jsx(DayCell, { day: day }, day.dateString))) }, weekIdx)))] }));
10
+ };
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+ interface BorderedBoxProps {
3
+ children: React.ReactNode;
4
+ borderColor: string;
5
+ backgroundColor?: string;
6
+ width?: number;
7
+ paddingX?: number;
8
+ paddingY?: number;
9
+ title?: string;
10
+ titleColor?: string;
11
+ }
12
+ /**
13
+ * A box component with double borders that properly applies backgroundColor
14
+ * to border characters. This fixes the issue where Ink's built-in borderStyle
15
+ * doesn't apply backgroundColor to border characters.
16
+ */
17
+ export declare const BorderedBox: React.FC<BorderedBoxProps>;
18
+ export {};
@@ -0,0 +1,29 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ /**
4
+ * A box component with double borders that properly applies backgroundColor
5
+ * to border characters. This fixes the issue where Ink's built-in borderStyle
6
+ * doesn't apply backgroundColor to border characters.
7
+ */
8
+ export const BorderedBox = ({ children, borderColor, backgroundColor, width, paddingX = 2, paddingY = 1, title, titleColor, }) => {
9
+ // Double border characters
10
+ const topLeft = "╔";
11
+ const topRight = "╗";
12
+ const bottomLeft = "╚";
13
+ const bottomRight = "╝";
14
+ const horizontal = "═";
15
+ const vertical = "║";
16
+ // Calculate inner width (content area)
17
+ // If width is specified, calculate inner width; otherwise use auto
18
+ const innerWidth = width ? width - 2 : undefined; // -2 for left and right borders
19
+ // Create horizontal border line
20
+ const createHorizontalBorder = (left, right, fill) => {
21
+ if (innerWidth) {
22
+ const fillCount = innerWidth;
23
+ return (_jsxs(Text, { color: borderColor, backgroundColor: backgroundColor, children: [left, fill.repeat(fillCount), right] }));
24
+ }
25
+ // For auto-width, we need to measure or use a reasonable default
26
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: borderColor, backgroundColor: backgroundColor, children: left }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: borderColor, backgroundColor: backgroundColor, children: fill.repeat(40) }) }), _jsx(Text, { color: borderColor, backgroundColor: backgroundColor, children: right })] }));
27
+ };
28
+ return (_jsxs(Box, { flexDirection: "column", backgroundColor: backgroundColor, children: [createHorizontalBorder(topLeft, topRight, horizontal), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: borderColor, backgroundColor: backgroundColor, children: vertical }), _jsxs(Box, { flexDirection: "column", width: innerWidth, paddingX: paddingX, paddingY: paddingY, backgroundColor: backgroundColor, children: [title && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: titleColor || borderColor, children: title }) })), children] }), _jsx(Text, { color: borderColor, backgroundColor: backgroundColor, children: vertical })] }), createHorizontalBorder(bottomLeft, bottomRight, horizontal)] }));
29
+ };
@@ -0,0 +1,2 @@
1
+ import React from "react";
2
+ export declare const ClearTimelineDialog: React.FC;
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { useTheme } from "../../contexts/ThemeContext";
4
+ import { useApp } from "../../contexts/AppContext";
5
+ import { Modal } from "./Modal";
6
+ import { getDateString } from "../../utils/date";
7
+ import { format } from "date-fns";
8
+ export const ClearTimelineDialog = () => {
9
+ const { theme } = useTheme();
10
+ const { selectedDate, timeline, setShowClearTimelineDialog, clearTimelineForDate, } = useApp();
11
+ const date = new Date(selectedDate.year, selectedDate.month, selectedDate.day);
12
+ const dateStr = getDateString(date);
13
+ const formattedDate = format(date, "MMMM do, yyyy");
14
+ const eventCount = (timeline[dateStr] || []).length;
15
+ const handleConfirm = () => {
16
+ clearTimelineForDate(dateStr);
17
+ setShowClearTimelineDialog(false);
18
+ };
19
+ const handleCancel = () => {
20
+ setShowClearTimelineDialog(false);
21
+ };
22
+ useInput((input, key) => {
23
+ if (input.toLowerCase() === "y" || key.return) {
24
+ handleConfirm();
25
+ }
26
+ else if (input.toLowerCase() === "n" || key.escape) {
27
+ handleCancel();
28
+ }
29
+ });
30
+ return (_jsx(Modal, { children: _jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: theme.colors.taskStateDelayed, paddingX: 4, paddingY: 2,
31
+ // @ts-ignore - backgroundColor is a valid Ink prop
32
+ backgroundColor: theme.colors.modalBackground || theme.colors.background, children: [_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.taskStateDelayed, children: "Clear Timeline" }) }), _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { color: theme.colors.foreground, children: "You are about to clear the timeline for" }), _jsx(Box, { justifyContent: "center", marginY: 1, children: _jsx(Text, { bold: true, color: theme.colors.calendarHeader, children: formattedDate }) }), eventCount > 0 ? (_jsxs(Text, { color: theme.colors.keyboardHint, children: ["This will remove ", eventCount, " event", eventCount !== 1 ? "s" : "", " from the timeline."] })) : (_jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: "The timeline is already empty." }))] }), _jsxs(Box, { marginTop: 2, justifyContent: "center", children: [_jsx(Text, { color: theme.colors.foreground, children: "Are you sure? " }), _jsx(Text, { color: theme.colors.taskStateCompleted, bold: true, children: "[Y]es" }), _jsx(Text, { color: theme.colors.foreground, children: " / " }), _jsx(Text, { color: theme.colors.taskStateDelayed, bold: true, children: "[N]o" })] }), _jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: "Press Y to confirm, N or Esc to cancel" }) })] }) }));
33
+ };
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ interface FullscreenBackgroundProps {
3
+ children: React.ReactNode;
4
+ backgroundColor: string;
5
+ }
6
+ /**
7
+ * A component that sets a fullscreen background color using Ink 6's
8
+ * native Box backgroundColor support. Supports hex colors, RGB, and named colors.
9
+ */
10
+ export declare const FullscreenBackground: React.FC<FullscreenBackgroundProps>;
11
+ export {};
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, useStdout } from "ink";
3
+ /**
4
+ * A component that sets a fullscreen background color using Ink 6's
5
+ * native Box backgroundColor support. Supports hex colors, RGB, and named colors.
6
+ */
7
+ export const FullscreenBackground = ({ children, backgroundColor, }) => {
8
+ const { stdout } = useStdout();
9
+ const width = stdout?.columns || 100;
10
+ const height = stdout?.rows || 30;
11
+ return (_jsx(Box, { flexDirection: "column", width: width, height: height, backgroundColor: backgroundColor, children: children }));
12
+ };
@@ -0,0 +1,2 @@
1
+ import React from "react";
2
+ export declare const HelpDialog: React.FC;
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { useTheme } from "../../contexts/ThemeContext";
4
+ import { Modal } from "./Modal";
5
+ export const HelpDialog = () => {
6
+ const { theme } = useTheme();
7
+ const shortcuts = [
8
+ { key: "Ctrl+C (twice)", action: "Quit application" },
9
+ { key: "?", action: "Toggle help dialog" },
10
+ { key: "Shift+;", action: "Show month overview" },
11
+ { key: "t", action: "Select theme" },
12
+ { key: "1", action: "Focus calendar pane" },
13
+ { key: "2 / Tab", action: "Focus tasks pane" },
14
+ { key: "3 / Shift+Tab", action: "Focus timeline pane" },
15
+ { key: "", action: "" },
16
+ { key: "Calendar Pane", action: "" },
17
+ { key: "h/l ←/→", action: "Navigate days" },
18
+ { key: "j/k ↓/↑", action: "Navigate weeks" },
19
+ { key: "n/p", action: "Next/prev month" },
20
+ { key: "T", action: "Go to today" },
21
+ { key: "", action: "" },
22
+ { key: "Tasks Pane", action: "" },
23
+ { key: "j/k ↓/↑", action: "Navigate tasks" },
24
+ { key: "a", action: "Add new task" },
25
+ { key: "e", action: "Edit task" },
26
+ { key: "d", action: "Delete task" },
27
+ { key: "Space", action: "Toggle completion" },
28
+ { key: "s", action: "Start task" },
29
+ { key: "D", action: "Mark delegated" },
30
+ { key: "x", action: "Mark delayed/cancelled" },
31
+ { key: "Enter", action: "Expand/collapse subtasks" },
32
+ { key: "", action: "" },
33
+ { key: "Timeline Pane", action: "" },
34
+ { key: "j/k", action: "Scroll timeline" },
35
+ { key: "Shift+C", action: "Clear timeline" },
36
+ ];
37
+ return (_jsx(Modal, { children: _jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: theme.colors.helpDialogBorder, paddingX: 2, paddingY: 1,
38
+ // @ts-ignore - backgroundColor is a valid Ink prop
39
+ backgroundColor: theme.colors.modalBackground || theme.colors.background, children: [_jsx(Text, { bold: true, color: theme.colors.calendarHeader, children: "Keyboard Shortcuts" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: shortcuts.map((item, idx) => (_jsx(Box, { marginY: 0, children: item.key ? (_jsxs(_Fragment, { children: [_jsx(Box, { width: 20, children: _jsx(Text, { color: theme.colors.timelineEventStarted, children: item.key }) }), _jsx(Text, { color: theme.colors.foreground, children: item.action })] })) : (_jsx(Text, { color: theme.colors.separator, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" })) }, idx))) }), _jsx(Box, { marginY: 1, children: _jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: "Press '?' to close" }) })] }) }));
40
+ };
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ interface ModalProps {
3
+ children: React.ReactNode;
4
+ }
5
+ /**
6
+ * A fullscreen overlay modal component that centers its content.
7
+ * This creates a floating dialog effect similar to OpenCode's command palette.
8
+ */
9
+ export declare const Modal: React.FC<ModalProps>;
10
+ export {};
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, useStdout } from "ink";
3
+ import { useTheme } from "../../contexts/ThemeContext";
4
+ import { FullscreenBackground } from "./FullscreenBackground";
5
+ /**
6
+ * A fullscreen overlay modal component that centers its content.
7
+ * This creates a floating dialog effect similar to OpenCode's command palette.
8
+ */
9
+ export const Modal = ({ children }) => {
10
+ const { theme } = useTheme();
11
+ const { stdout } = useStdout();
12
+ const width = stdout?.columns || 100;
13
+ const height = stdout?.rows || 30;
14
+ return (_jsx(FullscreenBackground, { backgroundColor: theme.colors.modalOverlay || "black", children: _jsx(Box, { flexDirection: "column", width: width, height: height, justifyContent: "center", alignItems: "center", children: children }) }));
15
+ };
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+ interface SeparatorProps {
3
+ vertical?: boolean;
4
+ height?: number;
5
+ width?: number;
6
+ }
7
+ export declare const Separator: React.FC<SeparatorProps>;
8
+ export {};
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { useTheme } from "../../contexts/ThemeContext";
4
+ export const Separator = ({ vertical = true, height, width = 1, }) => {
5
+ const { theme } = useTheme();
6
+ if (vertical) {
7
+ return (_jsx(Box, { flexDirection: "column", width: width, height: height, children: _jsx(Text, { color: theme.colors.separator, children: "\u2502" }) }));
8
+ }
9
+ return (_jsx(Box, { width: width, children: _jsx(Text, { color: theme.colors.separator, children: "\u2500" }) }));
10
+ };
@@ -0,0 +1,2 @@
1
+ import React from "react";
2
+ export declare const ThemeDialog: React.FC;
@@ -0,0 +1,117 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useMemo, useEffect } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import TextInput from "ink-text-input";
5
+ import { useTheme } from "../../contexts/ThemeContext";
6
+ import { useApp } from "../../contexts/AppContext";
7
+ import { Modal } from "./Modal";
8
+ import { getLightThemeNames, getDarkThemeNames } from "../../themes";
9
+ export const ThemeDialog = () => {
10
+ const { theme, setTheme, themeName } = useTheme();
11
+ const { setShowThemeDialog } = useApp();
12
+ const [searchQuery, setSearchQuery] = useState("");
13
+ const [focusMode, setFocusMode] = useState("search");
14
+ // Get organized theme lists
15
+ const lightThemes = useMemo(() => getLightThemeNames(), []);
16
+ const darkThemes = useMemo(() => getDarkThemeNames(), []);
17
+ // Combined list: light themes first, then dark themes
18
+ const allThemes = useMemo(() => {
19
+ // Filter themes based on search query
20
+ const query = searchQuery.toLowerCase();
21
+ const filteredLightThemes = lightThemes.filter((t) => t.toLowerCase().includes(query));
22
+ const filteredDarkThemes = darkThemes.filter((t) => t.toLowerCase().includes(query));
23
+ // Create a flat list with section markers
24
+ const items = [];
25
+ // Light themes section (only show if there are themes)
26
+ if (filteredLightThemes.length > 0) {
27
+ items.push({ type: "separator", value: "Light Themes" });
28
+ filteredLightThemes.forEach((t) => items.push({ type: "theme", value: t }));
29
+ }
30
+ // Dark themes section (only show if there are themes)
31
+ if (filteredDarkThemes.length > 0) {
32
+ items.push({ type: "separator", value: "Dark Themes" });
33
+ filteredDarkThemes.forEach((t) => items.push({ type: "theme", value: t }));
34
+ }
35
+ return items;
36
+ }, [lightThemes, darkThemes, searchQuery]);
37
+ // Get only theme items (for navigation)
38
+ const themeItems = useMemo(() => allThemes.filter((item) => item.type === "theme"), [allThemes]);
39
+ // Find initial selected index
40
+ const initialIndex = useMemo(() => {
41
+ const idx = themeItems.findIndex((item) => item.value === themeName);
42
+ return idx >= 0 ? idx : 0;
43
+ }, [themeItems, themeName]);
44
+ const [selectedIndex, setSelectedIndex] = useState(initialIndex);
45
+ // Reset selection when search query changes to highlight the first result
46
+ useEffect(() => {
47
+ setSelectedIndex(0);
48
+ }, [searchQuery]);
49
+ useInput((input, key) => {
50
+ // Escape closes dialog from both modes
51
+ if (key.escape) {
52
+ setShowThemeDialog(false);
53
+ return;
54
+ }
55
+ // Handle navigation based on focus mode
56
+ if (focusMode === "search") {
57
+ // Enter selects the first result and closes
58
+ if (key.return && themeItems.length > 0) {
59
+ setTheme(themeItems[0].value);
60
+ setShowThemeDialog(false);
61
+ return;
62
+ }
63
+ // In search mode, only down arrow moves to list (to avoid j/k interfering with typing)
64
+ if (key.downArrow && themeItems.length > 0) {
65
+ setFocusMode("list");
66
+ setSelectedIndex(0);
67
+ return;
68
+ }
69
+ }
70
+ else {
71
+ // In list mode
72
+ if (key.upArrow || input === "k") {
73
+ if (selectedIndex > 0) {
74
+ setSelectedIndex((prev) => prev - 1);
75
+ }
76
+ else {
77
+ // At top of list, move back to search
78
+ setFocusMode("search");
79
+ }
80
+ return;
81
+ }
82
+ if (key.downArrow || input === "j") {
83
+ setSelectedIndex((prev) => prev < themeItems.length - 1 ? prev + 1 : 0);
84
+ return;
85
+ }
86
+ // Enter selects theme only in list mode
87
+ if (key.return && themeItems.length > 0) {
88
+ setTheme(themeItems[selectedIndex].value);
89
+ setShowThemeDialog(false);
90
+ return;
91
+ }
92
+ }
93
+ }, { isActive: true });
94
+ // Get the currently selected theme name
95
+ const selectedThemeName = themeItems[selectedIndex]?.value;
96
+ // Format display name
97
+ const formatThemeName = (name) => {
98
+ return name
99
+ .split("-")
100
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
101
+ .join(" ");
102
+ };
103
+ return (_jsx(Modal, { children: _jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: theme.colors.helpDialogBorder, paddingX: 2, paddingY: 1, width: 44,
104
+ // @ts-ignore - backgroundColor is a valid Ink prop
105
+ backgroundColor: theme.colors.modalBackground || theme.colors.background, children: [_jsx(Text, { bold: true, color: theme.colors.calendarHeader, underline: true, children: "Select Theme" }), _jsxs(Box, { marginTop: 1, flexDirection: "row", alignItems: "center", children: [_jsxs(Text, { color: theme.colors.foreground, dimColor: true, children: ["Search:", " "] }), _jsx(Text, { color: theme.colors.foreground, children: _jsx(TextInput, { value: searchQuery, onChange: setSearchQuery, focus: focusMode === "search", placeholder: "Type to filter..." }) })] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: allThemes.map((item, idx) => {
106
+ if (item.type === "separator") {
107
+ return (_jsx(Box, { marginTop: idx > 0 ? 1 : 0, marginBottom: 0, children: _jsx(Text, { bold: true, color: theme.colors.calendarHeader, dimColor: true, children: item.value }) }, `sep-${idx}`));
108
+ }
109
+ const isSelected = item.value === selectedThemeName;
110
+ const isCurrent = item.value === themeName;
111
+ return (_jsx(Box, { paddingLeft: 1, children: _jsxs(Text, { color: isSelected
112
+ ? theme.colors.focusIndicator
113
+ : theme.colors.foreground, bold: isSelected, children: [isSelected ? "➜ " : " ", formatThemeName(item.value), isCurrent ? " (current)" : ""] }) }, item.value));
114
+ }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.keyboardHint, dimColor: true, children: focusMode === "search"
115
+ ? "Type to search • ↓ to navigate list • Esc to close"
116
+ : "↑/↓ or k/j to navigate • Enter to select • Esc to close" }) })] }) }));
117
+ };
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import { useTheme } from "../../contexts/ThemeContext";
3
+ interface ThemedScreenProps {
4
+ children: React.ReactNode;
5
+ backgroundColor?: string;
6
+ }
7
+ /**
8
+ * A wrapper component that applies the theme's background color to the entire screen.
9
+ *
10
+ * This works by:
11
+ * 1. Getting the terminal dimensions
12
+ * 2. Rendering the children
13
+ * 3. For any empty space, we don't try to fill it (terminals control their own bg)
14
+ *
15
+ * The key insight: we can't change the terminal's bg, but we CAN ensure all our
16
+ * Text elements have a consistent background by using a context or global style.
17
+ */
18
+ export declare const ThemedScreen: React.FC<ThemedScreenProps>;
19
+ /**
20
+ * Export the theme context hook for components that need to apply backgrounds
21
+ */
22
+ export { useTheme };
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useStdout } from "ink";
3
+ import { useTheme } from "../../contexts/ThemeContext";
4
+ /**
5
+ * A wrapper component that applies the theme's background color to the entire screen.
6
+ *
7
+ * This works by:
8
+ * 1. Getting the terminal dimensions
9
+ * 2. Rendering the children
10
+ * 3. For any empty space, we don't try to fill it (terminals control their own bg)
11
+ *
12
+ * The key insight: we can't change the terminal's bg, but we CAN ensure all our
13
+ * Text elements have a consistent background by using a context or global style.
14
+ */
15
+ export const ThemedScreen = ({ children, backgroundColor, }) => {
16
+ const { stdout } = useStdout();
17
+ const { theme } = useTheme();
18
+ const width = stdout?.columns || 100;
19
+ const height = stdout?.rows || 30;
20
+ // For 'terminal' theme, don't try to set any background
21
+ // For other themes, we simply render - the Text components should handle their own bg
22
+ const bgColor = backgroundColor ||
23
+ (theme.name === "terminal" ? undefined : theme.colors.background);
24
+ if (!bgColor) {
25
+ // Terminal theme - no background styling, use terminal's native colors
26
+ return (_jsx(Box, { flexDirection: "column", width: width, height: height, children: children }));
27
+ }
28
+ // For light/dark themes, we need to fill the background
29
+ // The trick: Use ANSI escape codes to fill the entire terminal background
30
+ // This ensures even the border areas get the background color
31
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { position: "absolute", width: width, height: height, children: Array.from({ length: height }).map((_, i) => (_jsx(Box, { width: width, children: _jsx(Text, { backgroundColor: bgColor, children: " ".repeat(width) }) }, `bg-row-${i}`))) }), _jsx(Box, { position: "relative", width: width, height: height, children: children })] }));
32
+ };
33
+ /**
34
+ * Export the theme context hook for components that need to apply backgrounds
35
+ */
36
+ export { useTheme };
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ import type { TextProps } from "ink";
3
+ /**
4
+ * A drop-in replacement for Ink's Text component that automatically applies
5
+ * the theme's background color. This ensures text is visible on themed backgrounds.
6
+ *
7
+ * Usage: Replace `import { Text } from 'ink'` with
8
+ * `import { Text } from '../common/ThemedText'`
9
+ */
10
+ export declare const ThemedText: React.FC<TextProps>;
11
+ export { ThemedText as Text };
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from "ink";
3
+ import { useTheme } from "../../contexts/ThemeContext";
4
+ /**
5
+ * A drop-in replacement for Ink's Text component that automatically applies
6
+ * the theme's background color. This ensures text is visible on themed backgrounds.
7
+ *
8
+ * Usage: Replace `import { Text } from 'ink'` with
9
+ * `import { Text } from '../common/ThemedText'`
10
+ */
11
+ export const ThemedText = ({ children, backgroundColor, ...props }) => {
12
+ const { theme } = useTheme();
13
+ // Only apply theme background for non-terminal themes
14
+ // If an explicit backgroundColor is passed, use that instead
15
+ const bgColor = backgroundColor ??
16
+ (theme.name !== "terminal" ? theme.colors.background : undefined);
17
+ return (_jsx(Text, { ...props, backgroundColor: bgColor, children: children }));
18
+ };
19
+ // Re-export as Text for easy drop-in replacement
20
+ export { ThemedText as Text };
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ import type { ReactNode } from "react";
3
+ interface PaneProps {
4
+ children: ReactNode;
5
+ title?: string;
6
+ isFocused?: boolean;
7
+ width?: number | string;
8
+ height?: number | string;
9
+ center?: boolean;
10
+ }
11
+ export declare const Pane: React.FC<PaneProps>;
12
+ export {};