floq 0.1.0 → 0.2.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.
- package/README.ja.md +89 -9
- package/README.md +89 -9
- package/dist/changelog.d.ts +13 -0
- package/dist/changelog.js +95 -0
- package/dist/cli.js +44 -1
- package/dist/commands/comment.d.ts +2 -0
- package/dist/commands/comment.js +67 -0
- package/dist/commands/config.d.ts +4 -0
- package/dist/commands/config.js +123 -1
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +13 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +40 -3
- package/dist/db/index.js +20 -0
- package/dist/db/schema.d.ts +83 -0
- package/dist/db/schema.js +6 -0
- package/dist/i18n/en.d.ts +237 -0
- package/dist/i18n/en.js +127 -3
- package/dist/i18n/ja.js +127 -3
- package/dist/index.js +14 -4
- package/dist/paths.d.ts +4 -0
- package/dist/paths.js +63 -5
- package/dist/ui/App.js +280 -25
- package/dist/ui/ModeSelector.d.ts +7 -0
- package/dist/ui/ModeSelector.js +37 -0
- package/dist/ui/SetupWizard.d.ts +6 -0
- package/dist/ui/SetupWizard.js +321 -0
- package/dist/ui/components/HelpModal.d.ts +2 -1
- package/dist/ui/components/HelpModal.js +118 -4
- package/dist/ui/components/KanbanBoard.d.ts +6 -0
- package/dist/ui/components/KanbanBoard.js +508 -0
- package/dist/ui/components/KanbanColumn.d.ts +12 -0
- package/dist/ui/components/KanbanColumn.js +11 -0
- package/dist/ui/components/ProgressBar.d.ts +7 -0
- package/dist/ui/components/ProgressBar.js +13 -0
- package/dist/ui/components/SearchBar.d.ts +8 -0
- package/dist/ui/components/SearchBar.js +11 -0
- package/dist/ui/components/SearchResults.d.ts +9 -0
- package/dist/ui/components/SearchResults.js +18 -0
- package/dist/ui/components/TaskItem.d.ts +6 -1
- package/dist/ui/components/TaskItem.js +3 -2
- package/package.json +1 -1
package/dist/ui/App.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect, useCallback } from 'react';
|
|
3
3
|
import { Box, Text, useInput, useApp } from 'ink';
|
|
4
4
|
import TextInput from 'ink-text-input';
|
|
@@ -7,15 +7,20 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
7
7
|
import { TaskItem } from './components/TaskItem.js';
|
|
8
8
|
import { HelpModal } from './components/HelpModal.js';
|
|
9
9
|
import { FunctionKeyBar } from './components/FunctionKeyBar.js';
|
|
10
|
+
import { SearchBar } from './components/SearchBar.js';
|
|
11
|
+
import { SearchResults } from './components/SearchResults.js';
|
|
10
12
|
import { SplashScreen } from './SplashScreen.js';
|
|
11
13
|
import { getDb, schema } from '../db/index.js';
|
|
12
14
|
import { t, fmt } from '../i18n/index.js';
|
|
13
15
|
import { ThemeProvider, useTheme } from './theme/index.js';
|
|
14
|
-
import { getThemeName, isTursoEnabled, getTursoConfig } from '../config.js';
|
|
15
|
-
|
|
16
|
+
import { getThemeName, getViewMode, isTursoEnabled, getTursoConfig } from '../config.js';
|
|
17
|
+
import { KanbanBoard } from './components/KanbanBoard.js';
|
|
18
|
+
import { VERSION } from '../version.js';
|
|
19
|
+
const TABS = ['inbox', 'next', 'waiting', 'someday', 'projects', 'done'];
|
|
16
20
|
export function App() {
|
|
17
21
|
const themeName = getThemeName();
|
|
18
|
-
|
|
22
|
+
const viewMode = getViewMode();
|
|
23
|
+
return (_jsx(ThemeProvider, { themeName: themeName, children: viewMode === 'kanban' ? _jsx(KanbanBoard, {}) : _jsx(AppContent, {}) }));
|
|
19
24
|
}
|
|
20
25
|
function AppContent() {
|
|
21
26
|
const theme = useTheme();
|
|
@@ -30,12 +35,22 @@ function AppContent() {
|
|
|
30
35
|
waiting: [],
|
|
31
36
|
someday: [],
|
|
32
37
|
projects: [],
|
|
38
|
+
done: [],
|
|
33
39
|
});
|
|
34
40
|
const [message, setMessage] = useState(null);
|
|
35
41
|
const [selectedProject, setSelectedProject] = useState(null);
|
|
36
42
|
const [projectTasks, setProjectTasks] = useState([]);
|
|
37
43
|
const [taskToLink, setTaskToLink] = useState(null);
|
|
38
44
|
const [projectSelectIndex, setProjectSelectIndex] = useState(0);
|
|
45
|
+
const [selectedTask, setSelectedTask] = useState(null);
|
|
46
|
+
const [taskComments, setTaskComments] = useState([]);
|
|
47
|
+
const [selectedCommentIndex, setSelectedCommentIndex] = useState(0);
|
|
48
|
+
const [taskToWaiting, setTaskToWaiting] = useState(null);
|
|
49
|
+
const [projectProgress, setProjectProgress] = useState({});
|
|
50
|
+
// Search state
|
|
51
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
52
|
+
const [searchResults, setSearchResults] = useState([]);
|
|
53
|
+
const [searchResultIndex, setSearchResultIndex] = useState(0);
|
|
39
54
|
const i18n = t();
|
|
40
55
|
const loadTasks = useCallback(async () => {
|
|
41
56
|
const db = getDb();
|
|
@@ -45,9 +60,10 @@ function AppContent() {
|
|
|
45
60
|
waiting: [],
|
|
46
61
|
someday: [],
|
|
47
62
|
projects: [],
|
|
63
|
+
done: [],
|
|
48
64
|
};
|
|
49
65
|
// Load all tasks (including project children) by status
|
|
50
|
-
const statusList = ['inbox', 'next', 'waiting', 'someday'];
|
|
66
|
+
const statusList = ['inbox', 'next', 'waiting', 'someday', 'done'];
|
|
51
67
|
for (const status of statusList) {
|
|
52
68
|
newTasks[status] = await db
|
|
53
69
|
.select()
|
|
@@ -59,6 +75,18 @@ function AppContent() {
|
|
|
59
75
|
.select()
|
|
60
76
|
.from(schema.tasks)
|
|
61
77
|
.where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.status, 'next')));
|
|
78
|
+
// Calculate progress for each project
|
|
79
|
+
const progress = {};
|
|
80
|
+
for (const project of newTasks.projects) {
|
|
81
|
+
const children = await db
|
|
82
|
+
.select()
|
|
83
|
+
.from(schema.tasks)
|
|
84
|
+
.where(eq(schema.tasks.parentId, project.id));
|
|
85
|
+
const total = children.length;
|
|
86
|
+
const completed = children.filter(t => t.status === 'done').length;
|
|
87
|
+
progress[project.id] = { completed, total };
|
|
88
|
+
}
|
|
89
|
+
setProjectProgress(progress);
|
|
62
90
|
setTasks(newTasks);
|
|
63
91
|
}, []);
|
|
64
92
|
// Get parent project for a task
|
|
@@ -75,11 +103,43 @@ function AppContent() {
|
|
|
75
103
|
.where(eq(schema.tasks.parentId, projectId));
|
|
76
104
|
setProjectTasks(children);
|
|
77
105
|
}, []);
|
|
106
|
+
const loadTaskComments = useCallback(async (taskId) => {
|
|
107
|
+
const db = getDb();
|
|
108
|
+
const comments = await db
|
|
109
|
+
.select()
|
|
110
|
+
.from(schema.comments)
|
|
111
|
+
.where(eq(schema.comments.taskId, taskId));
|
|
112
|
+
setTaskComments(comments);
|
|
113
|
+
}, []);
|
|
78
114
|
useEffect(() => {
|
|
79
115
|
loadTasks();
|
|
80
116
|
}, [loadTasks]);
|
|
81
117
|
const currentTab = TABS[currentListIndex];
|
|
82
118
|
const currentTasks = mode === 'project-detail' ? projectTasks : tasks[currentTab];
|
|
119
|
+
// Get all tasks for search (across all statuses)
|
|
120
|
+
const getAllTasks = useCallback(() => {
|
|
121
|
+
const allTasks = [];
|
|
122
|
+
for (const tab of TABS) {
|
|
123
|
+
allTasks.push(...tasks[tab]);
|
|
124
|
+
}
|
|
125
|
+
return allTasks;
|
|
126
|
+
}, [tasks]);
|
|
127
|
+
// Search tasks by query
|
|
128
|
+
const searchTasks = useCallback((query) => {
|
|
129
|
+
if (!query.trim())
|
|
130
|
+
return [];
|
|
131
|
+
const lowerQuery = query.toLowerCase();
|
|
132
|
+
const allTasks = getAllTasks();
|
|
133
|
+
return allTasks.filter(task => task.title.toLowerCase().includes(lowerQuery) ||
|
|
134
|
+
(task.description && task.description.toLowerCase().includes(lowerQuery)));
|
|
135
|
+
}, [getAllTasks]);
|
|
136
|
+
// Handle search query change
|
|
137
|
+
const handleSearchChange = useCallback((value) => {
|
|
138
|
+
setSearchQuery(value);
|
|
139
|
+
const results = searchTasks(value);
|
|
140
|
+
setSearchResults(results);
|
|
141
|
+
setSearchResultIndex(0);
|
|
142
|
+
}, [searchTasks]);
|
|
83
143
|
const addTask = useCallback(async (title, parentId) => {
|
|
84
144
|
if (!title.trim())
|
|
85
145
|
return;
|
|
@@ -97,9 +157,57 @@ function AppContent() {
|
|
|
97
157
|
setMessage(fmt(i18n.tui.added, { title: title.trim() }));
|
|
98
158
|
await loadTasks();
|
|
99
159
|
}, [i18n.tui.added, loadTasks]);
|
|
160
|
+
const addCommentToTask = useCallback(async (task, content) => {
|
|
161
|
+
const db = getDb();
|
|
162
|
+
await db.insert(schema.comments).values({
|
|
163
|
+
id: uuidv4(),
|
|
164
|
+
taskId: task.id,
|
|
165
|
+
content: content.trim(),
|
|
166
|
+
createdAt: new Date(),
|
|
167
|
+
});
|
|
168
|
+
setMessage(i18n.tui.commentAdded || 'Comment added');
|
|
169
|
+
await loadTaskComments(task.id);
|
|
170
|
+
}, [i18n.tui.commentAdded, loadTaskComments]);
|
|
171
|
+
const deleteComment = useCallback(async (comment) => {
|
|
172
|
+
const db = getDb();
|
|
173
|
+
await db.delete(schema.comments).where(eq(schema.comments.id, comment.id));
|
|
174
|
+
setMessage(i18n.tui.commentDeleted || 'Comment deleted');
|
|
175
|
+
if (selectedTask) {
|
|
176
|
+
await loadTaskComments(selectedTask.id);
|
|
177
|
+
}
|
|
178
|
+
}, [i18n.tui.commentDeleted, loadTaskComments, selectedTask]);
|
|
100
179
|
const handleInputSubmit = async (value) => {
|
|
180
|
+
if (mode === 'move-to-waiting' && taskToWaiting) {
|
|
181
|
+
if (value.trim()) {
|
|
182
|
+
await moveTaskToWaiting(taskToWaiting, value);
|
|
183
|
+
}
|
|
184
|
+
setTaskToWaiting(null);
|
|
185
|
+
setMode('normal');
|
|
186
|
+
setInputValue('');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Handle search mode submit
|
|
190
|
+
if (mode === 'search') {
|
|
191
|
+
if (searchResults.length > 0) {
|
|
192
|
+
const task = searchResults[searchResultIndex];
|
|
193
|
+
setSelectedTask(task);
|
|
194
|
+
loadTaskComments(task.id);
|
|
195
|
+
setMode('task-detail');
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
setMode('normal');
|
|
199
|
+
}
|
|
200
|
+
setSearchQuery('');
|
|
201
|
+
setSearchResults([]);
|
|
202
|
+
setSearchResultIndex(0);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
101
205
|
if (value.trim()) {
|
|
102
|
-
if (mode === 'add-
|
|
206
|
+
if (mode === 'add-comment' && selectedTask) {
|
|
207
|
+
await addCommentToTask(selectedTask, value);
|
|
208
|
+
setMode('task-detail');
|
|
209
|
+
}
|
|
210
|
+
else if (mode === 'add-to-project' && selectedProject) {
|
|
103
211
|
await addTask(value, selectedProject.id);
|
|
104
212
|
await loadProjectTasks(selectedProject.id);
|
|
105
213
|
setMode('project-detail');
|
|
@@ -110,7 +218,12 @@ function AppContent() {
|
|
|
110
218
|
}
|
|
111
219
|
}
|
|
112
220
|
else {
|
|
113
|
-
|
|
221
|
+
if (mode === 'add-comment') {
|
|
222
|
+
setMode('task-detail');
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
setMode(mode === 'add-to-project' ? 'project-detail' : 'normal');
|
|
226
|
+
}
|
|
114
227
|
}
|
|
115
228
|
setInputValue('');
|
|
116
229
|
};
|
|
@@ -138,6 +251,14 @@ function AppContent() {
|
|
|
138
251
|
setMessage(fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[status] }));
|
|
139
252
|
await loadTasks();
|
|
140
253
|
}, [i18n.tui.movedTo, i18n.status, loadTasks]);
|
|
254
|
+
const moveTaskToWaiting = useCallback(async (task, waitingFor) => {
|
|
255
|
+
const db = getDb();
|
|
256
|
+
await db.update(schema.tasks)
|
|
257
|
+
.set({ status: 'waiting', waitingFor: waitingFor.trim(), updatedAt: new Date() })
|
|
258
|
+
.where(eq(schema.tasks.id, task.id));
|
|
259
|
+
setMessage(fmt(i18n.tui.movedToWaiting, { title: task.title, person: waitingFor.trim() }));
|
|
260
|
+
await loadTasks();
|
|
261
|
+
}, [i18n.tui.movedToWaiting, loadTasks]);
|
|
141
262
|
const makeTaskProject = useCallback(async (task) => {
|
|
142
263
|
const db = getDb();
|
|
143
264
|
await db.update(schema.tasks)
|
|
@@ -158,6 +279,8 @@ function AppContent() {
|
|
|
158
279
|
return i18n.tui.tabSomeday;
|
|
159
280
|
case 'projects':
|
|
160
281
|
return i18n.tui.tabProjects || 'Projects';
|
|
282
|
+
case 'done':
|
|
283
|
+
return i18n.tui.tabDone || 'Done';
|
|
161
284
|
}
|
|
162
285
|
};
|
|
163
286
|
useInput((input, key) => {
|
|
@@ -166,25 +289,111 @@ function AppContent() {
|
|
|
166
289
|
setMode('normal');
|
|
167
290
|
return;
|
|
168
291
|
}
|
|
169
|
-
// Handle help mode -
|
|
292
|
+
// Handle help mode - let HelpModal handle input
|
|
170
293
|
if (mode === 'help') {
|
|
171
|
-
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
// Handle search mode
|
|
297
|
+
if (mode === 'search') {
|
|
298
|
+
if (key.escape) {
|
|
299
|
+
setSearchQuery('');
|
|
300
|
+
setSearchResults([]);
|
|
301
|
+
setSearchResultIndex(0);
|
|
302
|
+
setMode('normal');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// Navigate search results with Ctrl+j/k or Ctrl+n/p
|
|
306
|
+
if (key.ctrl && (input === 'j' || input === 'n')) {
|
|
307
|
+
setSearchResultIndex((prev) => prev < searchResults.length - 1 ? prev + 1 : 0);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (key.ctrl && (input === 'k' || input === 'p')) {
|
|
311
|
+
setSearchResultIndex((prev) => prev > 0 ? prev - 1 : Math.max(0, searchResults.length - 1));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// Let TextInput handle other keys
|
|
172
315
|
return;
|
|
173
316
|
}
|
|
174
317
|
// Handle add mode
|
|
175
|
-
if (mode === 'add' || mode === 'add-to-project') {
|
|
318
|
+
if (mode === 'add' || mode === 'add-to-project' || mode === 'add-comment' || mode === 'move-to-waiting') {
|
|
176
319
|
if (key.escape) {
|
|
177
320
|
setInputValue('');
|
|
178
|
-
|
|
321
|
+
if (mode === 'move-to-waiting') {
|
|
322
|
+
setTaskToWaiting(null);
|
|
323
|
+
setMode('normal');
|
|
324
|
+
}
|
|
325
|
+
else if (mode === 'add-comment') {
|
|
326
|
+
setMode('task-detail');
|
|
327
|
+
}
|
|
328
|
+
else if (mode === 'add-to-project') {
|
|
329
|
+
setMode('project-detail');
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
setMode('normal');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
// Handle task-detail mode
|
|
338
|
+
if (mode === 'task-detail') {
|
|
339
|
+
if (key.escape || key.backspace || input === 'b') {
|
|
340
|
+
// If came from project-detail, go back to project-detail
|
|
341
|
+
if (selectedProject) {
|
|
342
|
+
setMode('project-detail');
|
|
343
|
+
// Find the task index in project tasks
|
|
344
|
+
const taskIndex = projectTasks.findIndex(t => t.id === selectedTask?.id);
|
|
345
|
+
if (taskIndex >= 0) {
|
|
346
|
+
setSelectedTaskIndex(taskIndex);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
setMode('normal');
|
|
351
|
+
}
|
|
352
|
+
setSelectedTask(null);
|
|
353
|
+
setTaskComments([]);
|
|
354
|
+
setSelectedCommentIndex(0);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
// Navigate comments
|
|
358
|
+
if (key.upArrow || input === 'k') {
|
|
359
|
+
setSelectedCommentIndex((prev) => (prev > 0 ? prev - 1 : Math.max(0, taskComments.length - 1)));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (key.downArrow || input === 'j') {
|
|
363
|
+
setSelectedCommentIndex((prev) => (prev < taskComments.length - 1 ? prev + 1 : 0));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
// Delete comment
|
|
367
|
+
if (input === 'd' && taskComments.length > 0) {
|
|
368
|
+
const comment = taskComments[selectedCommentIndex];
|
|
369
|
+
deleteComment(comment).then(() => {
|
|
370
|
+
if (selectedCommentIndex >= taskComments.length - 1) {
|
|
371
|
+
setSelectedCommentIndex(Math.max(0, selectedCommentIndex - 1));
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
// Add comment
|
|
377
|
+
if (input === 'i') {
|
|
378
|
+
setMode('add-comment');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
// Link to project (P key)
|
|
382
|
+
if (input === 'P' && tasks.projects.length > 0) {
|
|
383
|
+
setTaskToLink(selectedTask);
|
|
384
|
+
setProjectSelectIndex(0);
|
|
385
|
+
setMode('select-project');
|
|
386
|
+
return;
|
|
179
387
|
}
|
|
180
388
|
return;
|
|
181
389
|
}
|
|
182
390
|
// Handle select-project mode
|
|
183
391
|
if (mode === 'select-project') {
|
|
184
392
|
if (key.escape) {
|
|
393
|
+
const wasFromTaskDetail = selectedTask !== null;
|
|
185
394
|
setTaskToLink(null);
|
|
186
395
|
setProjectSelectIndex(0);
|
|
187
|
-
setMode('normal');
|
|
396
|
+
setMode(wasFromTaskDetail ? 'task-detail' : 'normal');
|
|
188
397
|
return;
|
|
189
398
|
}
|
|
190
399
|
// Navigate projects
|
|
@@ -199,10 +408,11 @@ function AppContent() {
|
|
|
199
408
|
// Select project with Enter
|
|
200
409
|
if (key.return && taskToLink && tasks.projects.length > 0) {
|
|
201
410
|
const project = tasks.projects[projectSelectIndex];
|
|
411
|
+
const wasFromTaskDetail = selectedTask !== null;
|
|
202
412
|
linkTaskToProject(taskToLink, project);
|
|
203
413
|
setTaskToLink(null);
|
|
204
414
|
setProjectSelectIndex(0);
|
|
205
|
-
setMode('normal');
|
|
415
|
+
setMode(wasFromTaskDetail ? 'task-detail' : 'normal');
|
|
206
416
|
return;
|
|
207
417
|
}
|
|
208
418
|
return;
|
|
@@ -240,6 +450,14 @@ function AppContent() {
|
|
|
240
450
|
});
|
|
241
451
|
return;
|
|
242
452
|
}
|
|
453
|
+
// Enter to view task details
|
|
454
|
+
if (key.return && projectTasks.length > 0) {
|
|
455
|
+
const task = projectTasks[selectedTaskIndex];
|
|
456
|
+
setSelectedTask(task);
|
|
457
|
+
loadTaskComments(task.id);
|
|
458
|
+
setMode('task-detail');
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
243
461
|
return;
|
|
244
462
|
}
|
|
245
463
|
// Clear message on any input
|
|
@@ -294,6 +512,14 @@ function AppContent() {
|
|
|
294
512
|
setMode('help');
|
|
295
513
|
return;
|
|
296
514
|
}
|
|
515
|
+
// Search mode
|
|
516
|
+
if (input === '/') {
|
|
517
|
+
setMode('search');
|
|
518
|
+
setSearchQuery('');
|
|
519
|
+
setSearchResults([]);
|
|
520
|
+
setSearchResultIndex(0);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
297
523
|
// Quit
|
|
298
524
|
if (input === 'q' || (key.ctrl && input === 'c')) {
|
|
299
525
|
exit();
|
|
@@ -330,6 +556,11 @@ function AppContent() {
|
|
|
330
556
|
setSelectedTaskIndex(0);
|
|
331
557
|
return;
|
|
332
558
|
}
|
|
559
|
+
if (input === '6') {
|
|
560
|
+
setCurrentListIndex(5);
|
|
561
|
+
setSelectedTaskIndex(0);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
333
564
|
// Navigate between lists
|
|
334
565
|
if (key.leftArrow || input === 'h') {
|
|
335
566
|
setCurrentListIndex((prev) => (prev > 0 ? prev - 1 : TABS.length - 1));
|
|
@@ -359,8 +590,16 @@ function AppContent() {
|
|
|
359
590
|
setSelectedTaskIndex(0);
|
|
360
591
|
return;
|
|
361
592
|
}
|
|
593
|
+
// Enter to view task details (on non-projects tabs)
|
|
594
|
+
if (key.return && currentTab !== 'projects' && currentTasks.length > 0) {
|
|
595
|
+
const task = currentTasks[selectedTaskIndex];
|
|
596
|
+
setSelectedTask(task);
|
|
597
|
+
loadTaskComments(task.id);
|
|
598
|
+
setMode('task-detail');
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
362
601
|
// Mark as project (p key)
|
|
363
|
-
if (input === 'p' && currentTasks.length > 0 && currentTab !== 'projects') {
|
|
602
|
+
if (input === 'p' && currentTasks.length > 0 && currentTab !== 'projects' && currentTab !== 'done') {
|
|
364
603
|
const task = currentTasks[selectedTaskIndex];
|
|
365
604
|
makeTaskProject(task).then(() => {
|
|
366
605
|
if (selectedTaskIndex >= currentTasks.length - 1) {
|
|
@@ -370,15 +609,15 @@ function AppContent() {
|
|
|
370
609
|
return;
|
|
371
610
|
}
|
|
372
611
|
// Link to project (P key - shift+p)
|
|
373
|
-
if (input === 'P' && currentTasks.length > 0 && currentTab !== 'projects' && tasks.projects.length > 0) {
|
|
612
|
+
if (input === 'P' && currentTasks.length > 0 && currentTab !== 'projects' && currentTab !== 'done' && tasks.projects.length > 0) {
|
|
374
613
|
const task = currentTasks[selectedTaskIndex];
|
|
375
614
|
setTaskToLink(task);
|
|
376
615
|
setProjectSelectIndex(0);
|
|
377
616
|
setMode('select-project');
|
|
378
617
|
return;
|
|
379
618
|
}
|
|
380
|
-
// Mark as done
|
|
381
|
-
if (input === 'd' && currentTasks.length > 0) {
|
|
619
|
+
// Mark as done (not available on done tab)
|
|
620
|
+
if (input === 'd' && currentTasks.length > 0 && currentTab !== 'done') {
|
|
382
621
|
const task = currentTasks[selectedTaskIndex];
|
|
383
622
|
markTaskDone(task).then(() => {
|
|
384
623
|
if (selectedTaskIndex >= currentTasks.length - 1) {
|
|
@@ -388,7 +627,7 @@ function AppContent() {
|
|
|
388
627
|
return;
|
|
389
628
|
}
|
|
390
629
|
// Move to next actions
|
|
391
|
-
if (input === 'n' && currentTasks.length > 0 && currentTab !== 'next' && currentTab !== 'projects') {
|
|
630
|
+
if (input === 'n' && currentTasks.length > 0 && currentTab !== 'next' && currentTab !== 'projects' && currentTab !== 'done') {
|
|
392
631
|
const task = currentTasks[selectedTaskIndex];
|
|
393
632
|
moveTaskToStatus(task, 'next').then(() => {
|
|
394
633
|
if (selectedTaskIndex >= currentTasks.length - 1) {
|
|
@@ -398,7 +637,7 @@ function AppContent() {
|
|
|
398
637
|
return;
|
|
399
638
|
}
|
|
400
639
|
// Move to someday
|
|
401
|
-
if (input === 's' && currentTasks.length > 0 && currentTab !== 'someday' && currentTab !== 'projects') {
|
|
640
|
+
if (input === 's' && currentTasks.length > 0 && currentTab !== 'someday' && currentTab !== 'projects' && currentTab !== 'done') {
|
|
402
641
|
const task = currentTasks[selectedTaskIndex];
|
|
403
642
|
moveTaskToStatus(task, 'someday').then(() => {
|
|
404
643
|
if (selectedTaskIndex >= currentTasks.length - 1) {
|
|
@@ -407,8 +646,15 @@ function AppContent() {
|
|
|
407
646
|
});
|
|
408
647
|
return;
|
|
409
648
|
}
|
|
649
|
+
// Move to waiting
|
|
650
|
+
if (input === 'w' && currentTasks.length > 0 && currentTab !== 'waiting' && currentTab !== 'projects' && currentTab !== 'done') {
|
|
651
|
+
const task = currentTasks[selectedTaskIndex];
|
|
652
|
+
setTaskToWaiting(task);
|
|
653
|
+
setMode('move-to-waiting');
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
410
656
|
// Move to inbox
|
|
411
|
-
if (input === 'i' && currentTasks.length > 0 && currentTab !== 'inbox' && currentTab !== 'projects') {
|
|
657
|
+
if (input === 'i' && currentTasks.length > 0 && currentTab !== 'inbox' && currentTab !== 'projects' && currentTab !== 'done') {
|
|
412
658
|
const task = currentTasks[selectedTaskIndex];
|
|
413
659
|
moveTaskToStatus(task, 'inbox').then(() => {
|
|
414
660
|
if (selectedTaskIndex >= currentTasks.length - 1) {
|
|
@@ -451,15 +697,24 @@ function AppContent() {
|
|
|
451
697
|
}
|
|
452
698
|
return '';
|
|
453
699
|
})() : '';
|
|
454
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: formatTitle(i18n.tui.title) }), tursoEnabled && (_jsxs(Text, { color: theme.colors.accent, children: [theme.name === 'modern' ? ' ☁️ ' : ' [SYNC] ', tursoHost] })), !tursoEnabled && (_jsx(Text, { color: theme.colors.textMuted, children: theme.name === 'modern' ? ' 💾 local' : ' [LOCAL]' }))] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
|
|
700
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: formatTitle(i18n.tui.title) }), _jsx(Text, { color: theme.colors.textMuted, children: theme.name === 'modern' ? ` v${VERSION}` : ` VER ${VERSION}` }), tursoEnabled && (_jsxs(Text, { color: theme.colors.accent, children: [theme.name === 'modern' ? ' ☁️ ' : ' [SYNC] ', tursoHost] })), !tursoEnabled && (_jsx(Text, { color: theme.colors.textMuted, children: theme.name === 'modern' ? ' 💾 local' : ' [LOCAL]' }))] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
|
|
455
701
|
const isActive = index === currentListIndex && mode !== 'project-detail';
|
|
456
702
|
const count = tasks[tab].length;
|
|
457
703
|
const label = `${index + 1}:${getTabLabel(tab)}(${count})`;
|
|
458
704
|
return (_jsx(Box, { marginRight: 1, children: _jsx(Text, { color: isActive ? theme.colors.textSelected : theme.colors.textMuted, bold: isActive, inverse: isActive && theme.style.tabActiveInverse, children: formatTabLabel(` ${label} `, isActive) }) }, tab));
|
|
459
|
-
}) }), mode === 'project-detail' && selectedProject && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📁 ' : '>> ', selectedProject.title] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ")"] })] })), _jsx(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, minHeight:
|
|
705
|
+
}) }), mode === 'project-detail' && selectedProject && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📁 ' : '>> ', selectedProject.title] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ")"] })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.projectTasks || 'Tasks', " (", projectTasks.length, ")"] }) })] })), (mode === 'task-detail' || mode === 'add-comment') && selectedTask && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📋 ' : '>> ', i18n.tui.taskDetailTitle || 'Task Details'] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ", ", i18n.tui.commentHint || 'i: add comment', ")"] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Text, { color: theme.colors.textMuted, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` - ${selectedTask.waitingFor}`] })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.comments || 'Comments', " (", taskComments.length, ")"] }) }), _jsx(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, minHeight: 5, children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
|
|
706
|
+
const isSelected = index === selectedCommentIndex && mode === 'task-detail';
|
|
707
|
+
return (_jsxs(Box, { flexDirection: "row", marginBottom: 1, children: [_jsx(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.textMuted, children: isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.textMuted, children: ["[", comment.createdAt.toLocaleString(), "]"] }), _jsx(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: comment.content })] })] }, comment.id));
|
|
708
|
+
})) }), mode === 'add-comment' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.addComment || 'New comment: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] }))] })), mode === 'search' && searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery })), mode !== 'task-detail' && mode !== 'add-comment' && mode !== 'search' && (_jsx(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, minHeight: 10, children: currentTasks.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noTasks })) : (currentTasks.map((task, index) => {
|
|
460
709
|
const parentProject = getParentProject(task.parentId);
|
|
461
|
-
|
|
462
|
-
|
|
710
|
+
const progress = currentTab === 'projects' ? projectProgress[task.id] : undefined;
|
|
711
|
+
return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress }, task.id));
|
|
712
|
+
})) })), (mode === 'add' || mode === 'add-to-project') && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: mode === 'add-to-project' && selectedProject
|
|
463
713
|
? `${i18n.tui.newTask}[${selectedProject.title}] `
|
|
464
|
-
: i18n.tui.newTask }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.placeholder }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'select-project' && taskToLink && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, project.title] }, project.id))) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsx(Box, { marginTop: 1, children: theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, {
|
|
714
|
+
: i18n.tui.newTask }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.placeholder }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'move-to-waiting' && taskToWaiting && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.waitingFor }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'select-project' && taskToLink && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, project.title] }, project.id))) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsx(Box, { marginTop: 1, children: (mode === 'task-detail' || mode === 'add-comment') ? (theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, { keys: [
|
|
715
|
+
{ key: 'i', label: i18n.tui.keyBar.comment },
|
|
716
|
+
{ key: 'd', label: i18n.tui.keyBar.delete },
|
|
717
|
+
{ key: 'P', label: i18n.tui.keyBar.project },
|
|
718
|
+
{ key: 'b', label: i18n.tui.keyBar.back },
|
|
719
|
+
] })) : (_jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.taskDetailFooter }))) : theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, {})) : (_jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.footer })) })] }));
|
|
465
720
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { getViewMode } from '../config.js';
|
|
5
|
+
const VALID_VIEW_MODES = ['gtd', 'kanban'];
|
|
6
|
+
const modeDisplayNames = {
|
|
7
|
+
gtd: 'GTD (Getting Things Done)',
|
|
8
|
+
kanban: 'Kanban Board',
|
|
9
|
+
};
|
|
10
|
+
const modeDescriptions = {
|
|
11
|
+
gtd: 'Classic GTD workflow with Inbox, Next, Waiting, Someday lists',
|
|
12
|
+
kanban: '3-column kanban board view',
|
|
13
|
+
};
|
|
14
|
+
export function ModeSelector({ onSelect }) {
|
|
15
|
+
const currentMode = getViewMode();
|
|
16
|
+
const initialIndex = VALID_VIEW_MODES.indexOf(currentMode);
|
|
17
|
+
const [selectedIndex, setSelectedIndex] = useState(initialIndex >= 0 ? initialIndex : 0);
|
|
18
|
+
useInput((input, key) => {
|
|
19
|
+
// j or down arrow: move down
|
|
20
|
+
if (input === 'j' || key.downArrow) {
|
|
21
|
+
setSelectedIndex((prev) => (prev < VALID_VIEW_MODES.length - 1 ? prev + 1 : 0));
|
|
22
|
+
}
|
|
23
|
+
// k or up arrow: move up
|
|
24
|
+
if (input === 'k' || key.upArrow) {
|
|
25
|
+
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : VALID_VIEW_MODES.length - 1));
|
|
26
|
+
}
|
|
27
|
+
// Enter: select
|
|
28
|
+
if (key.return) {
|
|
29
|
+
onSelect(VALID_VIEW_MODES[selectedIndex]);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Select a view mode:" }), _jsx(Text, { dimColor: true, children: "j/k: select, Enter: confirm" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: VALID_VIEW_MODES.map((mode, index) => {
|
|
33
|
+
const isSelected = index === selectedIndex;
|
|
34
|
+
const isCurrent = mode === currentMode;
|
|
35
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: isSelected ? 'cyan' : undefined, children: [isSelected ? '› ' : ' ', modeDisplayNames[mode], isCurrent ? ' (current)' : ''] }), _jsxs(Text, { dimColor: true, children: [' ', modeDescriptions[mode]] })] }, mode));
|
|
36
|
+
}) })] }));
|
|
37
|
+
}
|