floq 0.1.0 → 0.2.1
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 +278 -0
- package/dist/i18n/en.js +148 -3
- package/dist/i18n/ja.js +148 -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 +320 -37
- 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 +155 -4
- package/dist/ui/components/KanbanBoard.d.ts +6 -0
- package/dist/ui/components/KanbanBoard.js +498 -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,
|
|
15
|
-
|
|
16
|
+
import { getThemeName, getViewMode, isTursoEnabled } 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,23 @@ 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 [taskToDelete, setTaskToDelete] = useState(null);
|
|
50
|
+
const [projectProgress, setProjectProgress] = useState({});
|
|
51
|
+
// Search state
|
|
52
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
53
|
+
const [searchResults, setSearchResults] = useState([]);
|
|
54
|
+
const [searchResultIndex, setSearchResultIndex] = useState(0);
|
|
39
55
|
const i18n = t();
|
|
40
56
|
const loadTasks = useCallback(async () => {
|
|
41
57
|
const db = getDb();
|
|
@@ -45,9 +61,10 @@ function AppContent() {
|
|
|
45
61
|
waiting: [],
|
|
46
62
|
someday: [],
|
|
47
63
|
projects: [],
|
|
64
|
+
done: [],
|
|
48
65
|
};
|
|
49
66
|
// Load all tasks (including project children) by status
|
|
50
|
-
const statusList = ['inbox', 'next', 'waiting', 'someday'];
|
|
67
|
+
const statusList = ['inbox', 'next', 'waiting', 'someday', 'done'];
|
|
51
68
|
for (const status of statusList) {
|
|
52
69
|
newTasks[status] = await db
|
|
53
70
|
.select()
|
|
@@ -59,6 +76,18 @@ function AppContent() {
|
|
|
59
76
|
.select()
|
|
60
77
|
.from(schema.tasks)
|
|
61
78
|
.where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.status, 'next')));
|
|
79
|
+
// Calculate progress for each project
|
|
80
|
+
const progress = {};
|
|
81
|
+
for (const project of newTasks.projects) {
|
|
82
|
+
const children = await db
|
|
83
|
+
.select()
|
|
84
|
+
.from(schema.tasks)
|
|
85
|
+
.where(eq(schema.tasks.parentId, project.id));
|
|
86
|
+
const total = children.length;
|
|
87
|
+
const completed = children.filter(t => t.status === 'done').length;
|
|
88
|
+
progress[project.id] = { completed, total };
|
|
89
|
+
}
|
|
90
|
+
setProjectProgress(progress);
|
|
62
91
|
setTasks(newTasks);
|
|
63
92
|
}, []);
|
|
64
93
|
// Get parent project for a task
|
|
@@ -75,11 +104,43 @@ function AppContent() {
|
|
|
75
104
|
.where(eq(schema.tasks.parentId, projectId));
|
|
76
105
|
setProjectTasks(children);
|
|
77
106
|
}, []);
|
|
107
|
+
const loadTaskComments = useCallback(async (taskId) => {
|
|
108
|
+
const db = getDb();
|
|
109
|
+
const comments = await db
|
|
110
|
+
.select()
|
|
111
|
+
.from(schema.comments)
|
|
112
|
+
.where(eq(schema.comments.taskId, taskId));
|
|
113
|
+
setTaskComments(comments);
|
|
114
|
+
}, []);
|
|
78
115
|
useEffect(() => {
|
|
79
116
|
loadTasks();
|
|
80
117
|
}, [loadTasks]);
|
|
81
118
|
const currentTab = TABS[currentListIndex];
|
|
82
119
|
const currentTasks = mode === 'project-detail' ? projectTasks : tasks[currentTab];
|
|
120
|
+
// Get all tasks for search (across all statuses)
|
|
121
|
+
const getAllTasks = useCallback(() => {
|
|
122
|
+
const allTasks = [];
|
|
123
|
+
for (const tab of TABS) {
|
|
124
|
+
allTasks.push(...tasks[tab]);
|
|
125
|
+
}
|
|
126
|
+
return allTasks;
|
|
127
|
+
}, [tasks]);
|
|
128
|
+
// Search tasks by query
|
|
129
|
+
const searchTasks = useCallback((query) => {
|
|
130
|
+
if (!query.trim())
|
|
131
|
+
return [];
|
|
132
|
+
const lowerQuery = query.toLowerCase();
|
|
133
|
+
const allTasks = getAllTasks();
|
|
134
|
+
return allTasks.filter(task => task.title.toLowerCase().includes(lowerQuery) ||
|
|
135
|
+
(task.description && task.description.toLowerCase().includes(lowerQuery)));
|
|
136
|
+
}, [getAllTasks]);
|
|
137
|
+
// Handle search query change
|
|
138
|
+
const handleSearchChange = useCallback((value) => {
|
|
139
|
+
setSearchQuery(value);
|
|
140
|
+
const results = searchTasks(value);
|
|
141
|
+
setSearchResults(results);
|
|
142
|
+
setSearchResultIndex(0);
|
|
143
|
+
}, [searchTasks]);
|
|
83
144
|
const addTask = useCallback(async (title, parentId) => {
|
|
84
145
|
if (!title.trim())
|
|
85
146
|
return;
|
|
@@ -97,9 +158,57 @@ function AppContent() {
|
|
|
97
158
|
setMessage(fmt(i18n.tui.added, { title: title.trim() }));
|
|
98
159
|
await loadTasks();
|
|
99
160
|
}, [i18n.tui.added, loadTasks]);
|
|
161
|
+
const addCommentToTask = useCallback(async (task, content) => {
|
|
162
|
+
const db = getDb();
|
|
163
|
+
await db.insert(schema.comments).values({
|
|
164
|
+
id: uuidv4(),
|
|
165
|
+
taskId: task.id,
|
|
166
|
+
content: content.trim(),
|
|
167
|
+
createdAt: new Date(),
|
|
168
|
+
});
|
|
169
|
+
setMessage(i18n.tui.commentAdded || 'Comment added');
|
|
170
|
+
await loadTaskComments(task.id);
|
|
171
|
+
}, [i18n.tui.commentAdded, loadTaskComments]);
|
|
172
|
+
const deleteComment = useCallback(async (comment) => {
|
|
173
|
+
const db = getDb();
|
|
174
|
+
await db.delete(schema.comments).where(eq(schema.comments.id, comment.id));
|
|
175
|
+
setMessage(i18n.tui.commentDeleted || 'Comment deleted');
|
|
176
|
+
if (selectedTask) {
|
|
177
|
+
await loadTaskComments(selectedTask.id);
|
|
178
|
+
}
|
|
179
|
+
}, [i18n.tui.commentDeleted, loadTaskComments, selectedTask]);
|
|
100
180
|
const handleInputSubmit = async (value) => {
|
|
181
|
+
if (mode === 'move-to-waiting' && taskToWaiting) {
|
|
182
|
+
if (value.trim()) {
|
|
183
|
+
await moveTaskToWaiting(taskToWaiting, value);
|
|
184
|
+
}
|
|
185
|
+
setTaskToWaiting(null);
|
|
186
|
+
setMode('normal');
|
|
187
|
+
setInputValue('');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// Handle search mode submit
|
|
191
|
+
if (mode === 'search') {
|
|
192
|
+
if (searchResults.length > 0) {
|
|
193
|
+
const task = searchResults[searchResultIndex];
|
|
194
|
+
setSelectedTask(task);
|
|
195
|
+
loadTaskComments(task.id);
|
|
196
|
+
setMode('task-detail');
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
setMode('normal');
|
|
200
|
+
}
|
|
201
|
+
setSearchQuery('');
|
|
202
|
+
setSearchResults([]);
|
|
203
|
+
setSearchResultIndex(0);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
101
206
|
if (value.trim()) {
|
|
102
|
-
if (mode === 'add-
|
|
207
|
+
if (mode === 'add-comment' && selectedTask) {
|
|
208
|
+
await addCommentToTask(selectedTask, value);
|
|
209
|
+
setMode('task-detail');
|
|
210
|
+
}
|
|
211
|
+
else if (mode === 'add-to-project' && selectedProject) {
|
|
103
212
|
await addTask(value, selectedProject.id);
|
|
104
213
|
await loadProjectTasks(selectedProject.id);
|
|
105
214
|
setMode('project-detail');
|
|
@@ -110,7 +219,12 @@ function AppContent() {
|
|
|
110
219
|
}
|
|
111
220
|
}
|
|
112
221
|
else {
|
|
113
|
-
|
|
222
|
+
if (mode === 'add-comment') {
|
|
223
|
+
setMode('task-detail');
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
setMode(mode === 'add-to-project' ? 'project-detail' : 'normal');
|
|
227
|
+
}
|
|
114
228
|
}
|
|
115
229
|
setInputValue('');
|
|
116
230
|
};
|
|
@@ -138,6 +252,14 @@ function AppContent() {
|
|
|
138
252
|
setMessage(fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[status] }));
|
|
139
253
|
await loadTasks();
|
|
140
254
|
}, [i18n.tui.movedTo, i18n.status, loadTasks]);
|
|
255
|
+
const moveTaskToWaiting = useCallback(async (task, waitingFor) => {
|
|
256
|
+
const db = getDb();
|
|
257
|
+
await db.update(schema.tasks)
|
|
258
|
+
.set({ status: 'waiting', waitingFor: waitingFor.trim(), updatedAt: new Date() })
|
|
259
|
+
.where(eq(schema.tasks.id, task.id));
|
|
260
|
+
setMessage(fmt(i18n.tui.movedToWaiting, { title: task.title, person: waitingFor.trim() }));
|
|
261
|
+
await loadTasks();
|
|
262
|
+
}, [i18n.tui.movedToWaiting, loadTasks]);
|
|
141
263
|
const makeTaskProject = useCallback(async (task) => {
|
|
142
264
|
const db = getDb();
|
|
143
265
|
await db.update(schema.tasks)
|
|
@@ -146,6 +268,15 @@ function AppContent() {
|
|
|
146
268
|
setMessage(fmt(i18n.tui.madeProject || 'Made project: {title}', { title: task.title }));
|
|
147
269
|
await loadTasks();
|
|
148
270
|
}, [i18n.tui.madeProject, loadTasks]);
|
|
271
|
+
const deleteTask = useCallback(async (task) => {
|
|
272
|
+
const db = getDb();
|
|
273
|
+
// Delete comments first
|
|
274
|
+
await db.delete(schema.comments).where(eq(schema.comments.taskId, task.id));
|
|
275
|
+
// Delete the task
|
|
276
|
+
await db.delete(schema.tasks).where(eq(schema.tasks.id, task.id));
|
|
277
|
+
setMessage(fmt(i18n.tui.deleted || 'Deleted: "{title}"', { title: task.title }));
|
|
278
|
+
await loadTasks();
|
|
279
|
+
}, [i18n.tui.deleted, loadTasks]);
|
|
149
280
|
const getTabLabel = (tab) => {
|
|
150
281
|
switch (tab) {
|
|
151
282
|
case 'inbox':
|
|
@@ -158,6 +289,8 @@ function AppContent() {
|
|
|
158
289
|
return i18n.tui.tabSomeday;
|
|
159
290
|
case 'projects':
|
|
160
291
|
return i18n.tui.tabProjects || 'Projects';
|
|
292
|
+
case 'done':
|
|
293
|
+
return i18n.tui.tabDone || 'Done';
|
|
161
294
|
}
|
|
162
295
|
};
|
|
163
296
|
useInput((input, key) => {
|
|
@@ -166,25 +299,132 @@ function AppContent() {
|
|
|
166
299
|
setMode('normal');
|
|
167
300
|
return;
|
|
168
301
|
}
|
|
169
|
-
// Handle help mode -
|
|
302
|
+
// Handle help mode - let HelpModal handle input
|
|
170
303
|
if (mode === 'help') {
|
|
171
|
-
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// Handle search mode
|
|
307
|
+
if (mode === 'search') {
|
|
308
|
+
if (key.escape) {
|
|
309
|
+
setSearchQuery('');
|
|
310
|
+
setSearchResults([]);
|
|
311
|
+
setSearchResultIndex(0);
|
|
312
|
+
setMode('normal');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// Navigate search results with Ctrl+j/k or Ctrl+n/p
|
|
316
|
+
if (key.ctrl && (input === 'j' || input === 'n')) {
|
|
317
|
+
setSearchResultIndex((prev) => prev < searchResults.length - 1 ? prev + 1 : 0);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (key.ctrl && (input === 'k' || input === 'p')) {
|
|
321
|
+
setSearchResultIndex((prev) => prev > 0 ? prev - 1 : Math.max(0, searchResults.length - 1));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
// Let TextInput handle other keys
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
// Handle confirm-delete mode
|
|
328
|
+
if (mode === 'confirm-delete' && taskToDelete) {
|
|
329
|
+
if (input === 'y' || input === 'Y') {
|
|
330
|
+
deleteTask(taskToDelete).then(() => {
|
|
331
|
+
if (selectedTaskIndex >= currentTasks.length - 1) {
|
|
332
|
+
setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
setTaskToDelete(null);
|
|
336
|
+
setMode('normal');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (input === 'n' || input === 'N' || key.escape) {
|
|
340
|
+
setMessage(i18n.tui.deleteCancelled || 'Delete cancelled');
|
|
341
|
+
setTaskToDelete(null);
|
|
342
|
+
setMode('normal');
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Ignore other keys in confirm mode
|
|
172
346
|
return;
|
|
173
347
|
}
|
|
174
348
|
// Handle add mode
|
|
175
|
-
if (mode === 'add' || mode === 'add-to-project') {
|
|
349
|
+
if (mode === 'add' || mode === 'add-to-project' || mode === 'add-comment' || mode === 'move-to-waiting') {
|
|
176
350
|
if (key.escape) {
|
|
177
351
|
setInputValue('');
|
|
178
|
-
|
|
352
|
+
if (mode === 'move-to-waiting') {
|
|
353
|
+
setTaskToWaiting(null);
|
|
354
|
+
setMode('normal');
|
|
355
|
+
}
|
|
356
|
+
else if (mode === 'add-comment') {
|
|
357
|
+
setMode('task-detail');
|
|
358
|
+
}
|
|
359
|
+
else if (mode === 'add-to-project') {
|
|
360
|
+
setMode('project-detail');
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
setMode('normal');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
// Handle task-detail mode
|
|
369
|
+
if (mode === 'task-detail') {
|
|
370
|
+
if (key.escape || key.backspace || input === 'b') {
|
|
371
|
+
// If came from project-detail, go back to project-detail
|
|
372
|
+
if (selectedProject) {
|
|
373
|
+
setMode('project-detail');
|
|
374
|
+
// Find the task index in project tasks
|
|
375
|
+
const taskIndex = projectTasks.findIndex(t => t.id === selectedTask?.id);
|
|
376
|
+
if (taskIndex >= 0) {
|
|
377
|
+
setSelectedTaskIndex(taskIndex);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
setMode('normal');
|
|
382
|
+
}
|
|
383
|
+
setSelectedTask(null);
|
|
384
|
+
setTaskComments([]);
|
|
385
|
+
setSelectedCommentIndex(0);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
// Navigate comments
|
|
389
|
+
if (key.upArrow || input === 'k') {
|
|
390
|
+
setSelectedCommentIndex((prev) => (prev > 0 ? prev - 1 : Math.max(0, taskComments.length - 1)));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (key.downArrow || input === 'j') {
|
|
394
|
+
setSelectedCommentIndex((prev) => (prev < taskComments.length - 1 ? prev + 1 : 0));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
// Delete comment
|
|
398
|
+
if (input === 'd' && taskComments.length > 0) {
|
|
399
|
+
const comment = taskComments[selectedCommentIndex];
|
|
400
|
+
deleteComment(comment).then(() => {
|
|
401
|
+
if (selectedCommentIndex >= taskComments.length - 1) {
|
|
402
|
+
setSelectedCommentIndex(Math.max(0, selectedCommentIndex - 1));
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
// Add comment
|
|
408
|
+
if (input === 'i') {
|
|
409
|
+
setMode('add-comment');
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
// Link to project (P key)
|
|
413
|
+
if (input === 'P' && tasks.projects.length > 0) {
|
|
414
|
+
setTaskToLink(selectedTask);
|
|
415
|
+
setProjectSelectIndex(0);
|
|
416
|
+
setMode('select-project');
|
|
417
|
+
return;
|
|
179
418
|
}
|
|
180
419
|
return;
|
|
181
420
|
}
|
|
182
421
|
// Handle select-project mode
|
|
183
422
|
if (mode === 'select-project') {
|
|
184
423
|
if (key.escape) {
|
|
424
|
+
const wasFromTaskDetail = selectedTask !== null;
|
|
185
425
|
setTaskToLink(null);
|
|
186
426
|
setProjectSelectIndex(0);
|
|
187
|
-
setMode('normal');
|
|
427
|
+
setMode(wasFromTaskDetail ? 'task-detail' : 'normal');
|
|
188
428
|
return;
|
|
189
429
|
}
|
|
190
430
|
// Navigate projects
|
|
@@ -199,10 +439,11 @@ function AppContent() {
|
|
|
199
439
|
// Select project with Enter
|
|
200
440
|
if (key.return && taskToLink && tasks.projects.length > 0) {
|
|
201
441
|
const project = tasks.projects[projectSelectIndex];
|
|
442
|
+
const wasFromTaskDetail = selectedTask !== null;
|
|
202
443
|
linkTaskToProject(taskToLink, project);
|
|
203
444
|
setTaskToLink(null);
|
|
204
445
|
setProjectSelectIndex(0);
|
|
205
|
-
setMode('normal');
|
|
446
|
+
setMode(wasFromTaskDetail ? 'task-detail' : 'normal');
|
|
206
447
|
return;
|
|
207
448
|
}
|
|
208
449
|
return;
|
|
@@ -240,6 +481,14 @@ function AppContent() {
|
|
|
240
481
|
});
|
|
241
482
|
return;
|
|
242
483
|
}
|
|
484
|
+
// Enter to view task details
|
|
485
|
+
if (key.return && projectTasks.length > 0) {
|
|
486
|
+
const task = projectTasks[selectedTaskIndex];
|
|
487
|
+
setSelectedTask(task);
|
|
488
|
+
loadTaskComments(task.id);
|
|
489
|
+
setMode('task-detail');
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
243
492
|
return;
|
|
244
493
|
}
|
|
245
494
|
// Clear message on any input
|
|
@@ -294,6 +543,14 @@ function AppContent() {
|
|
|
294
543
|
setMode('help');
|
|
295
544
|
return;
|
|
296
545
|
}
|
|
546
|
+
// Search mode
|
|
547
|
+
if (input === '/') {
|
|
548
|
+
setMode('search');
|
|
549
|
+
setSearchQuery('');
|
|
550
|
+
setSearchResults([]);
|
|
551
|
+
setSearchResultIndex(0);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
297
554
|
// Quit
|
|
298
555
|
if (input === 'q' || (key.ctrl && input === 'c')) {
|
|
299
556
|
exit();
|
|
@@ -330,6 +587,11 @@ function AppContent() {
|
|
|
330
587
|
setSelectedTaskIndex(0);
|
|
331
588
|
return;
|
|
332
589
|
}
|
|
590
|
+
if (input === '6') {
|
|
591
|
+
setCurrentListIndex(5);
|
|
592
|
+
setSelectedTaskIndex(0);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
333
595
|
// Navigate between lists
|
|
334
596
|
if (key.leftArrow || input === 'h') {
|
|
335
597
|
setCurrentListIndex((prev) => (prev > 0 ? prev - 1 : TABS.length - 1));
|
|
@@ -359,8 +621,16 @@ function AppContent() {
|
|
|
359
621
|
setSelectedTaskIndex(0);
|
|
360
622
|
return;
|
|
361
623
|
}
|
|
624
|
+
// Enter to view task details (on non-projects tabs)
|
|
625
|
+
if (key.return && currentTab !== 'projects' && currentTasks.length > 0) {
|
|
626
|
+
const task = currentTasks[selectedTaskIndex];
|
|
627
|
+
setSelectedTask(task);
|
|
628
|
+
loadTaskComments(task.id);
|
|
629
|
+
setMode('task-detail');
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
362
632
|
// Mark as project (p key)
|
|
363
|
-
if (input === 'p' && currentTasks.length > 0 && currentTab !== 'projects') {
|
|
633
|
+
if (input === 'p' && currentTasks.length > 0 && currentTab !== 'projects' && currentTab !== 'done') {
|
|
364
634
|
const task = currentTasks[selectedTaskIndex];
|
|
365
635
|
makeTaskProject(task).then(() => {
|
|
366
636
|
if (selectedTaskIndex >= currentTasks.length - 1) {
|
|
@@ -370,15 +640,15 @@ function AppContent() {
|
|
|
370
640
|
return;
|
|
371
641
|
}
|
|
372
642
|
// Link to project (P key - shift+p)
|
|
373
|
-
if (input === 'P' && currentTasks.length > 0 && currentTab !== 'projects' && tasks.projects.length > 0) {
|
|
643
|
+
if (input === 'P' && currentTasks.length > 0 && currentTab !== 'projects' && currentTab !== 'done' && tasks.projects.length > 0) {
|
|
374
644
|
const task = currentTasks[selectedTaskIndex];
|
|
375
645
|
setTaskToLink(task);
|
|
376
646
|
setProjectSelectIndex(0);
|
|
377
647
|
setMode('select-project');
|
|
378
648
|
return;
|
|
379
649
|
}
|
|
380
|
-
// Mark as done
|
|
381
|
-
if (input === 'd' && currentTasks.length > 0) {
|
|
650
|
+
// Mark as done (not available on done tab)
|
|
651
|
+
if (input === 'd' && currentTasks.length > 0 && currentTab !== 'done') {
|
|
382
652
|
const task = currentTasks[selectedTaskIndex];
|
|
383
653
|
markTaskDone(task).then(() => {
|
|
384
654
|
if (selectedTaskIndex >= currentTasks.length - 1) {
|
|
@@ -387,8 +657,15 @@ function AppContent() {
|
|
|
387
657
|
});
|
|
388
658
|
return;
|
|
389
659
|
}
|
|
660
|
+
// Delete task (D key - with confirmation)
|
|
661
|
+
if (input === 'D' && currentTasks.length > 0 && currentTab !== 'projects') {
|
|
662
|
+
const task = currentTasks[selectedTaskIndex];
|
|
663
|
+
setTaskToDelete(task);
|
|
664
|
+
setMode('confirm-delete');
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
390
667
|
// Move to next actions
|
|
391
|
-
if (input === 'n' && currentTasks.length > 0 && currentTab !== 'next' && currentTab !== 'projects') {
|
|
668
|
+
if (input === 'n' && currentTasks.length > 0 && currentTab !== 'next' && currentTab !== 'projects' && currentTab !== 'done') {
|
|
392
669
|
const task = currentTasks[selectedTaskIndex];
|
|
393
670
|
moveTaskToStatus(task, 'next').then(() => {
|
|
394
671
|
if (selectedTaskIndex >= currentTasks.length - 1) {
|
|
@@ -398,7 +675,7 @@ function AppContent() {
|
|
|
398
675
|
return;
|
|
399
676
|
}
|
|
400
677
|
// Move to someday
|
|
401
|
-
if (input === 's' && currentTasks.length > 0 && currentTab !== 'someday' && currentTab !== 'projects') {
|
|
678
|
+
if (input === 's' && currentTasks.length > 0 && currentTab !== 'someday' && currentTab !== 'projects' && currentTab !== 'done') {
|
|
402
679
|
const task = currentTasks[selectedTaskIndex];
|
|
403
680
|
moveTaskToStatus(task, 'someday').then(() => {
|
|
404
681
|
if (selectedTaskIndex >= currentTasks.length - 1) {
|
|
@@ -407,8 +684,15 @@ function AppContent() {
|
|
|
407
684
|
});
|
|
408
685
|
return;
|
|
409
686
|
}
|
|
687
|
+
// Move to waiting
|
|
688
|
+
if (input === 'w' && currentTasks.length > 0 && currentTab !== 'waiting' && currentTab !== 'projects' && currentTab !== 'done') {
|
|
689
|
+
const task = currentTasks[selectedTaskIndex];
|
|
690
|
+
setTaskToWaiting(task);
|
|
691
|
+
setMode('move-to-waiting');
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
410
694
|
// Move to inbox
|
|
411
|
-
if (input === 'i' && currentTasks.length > 0 && currentTab !== 'inbox' && currentTab !== 'projects') {
|
|
695
|
+
if (input === 'i' && currentTasks.length > 0 && currentTab !== 'inbox' && currentTab !== 'projects' && currentTab !== 'done') {
|
|
412
696
|
const task = currentTasks[selectedTaskIndex];
|
|
413
697
|
moveTaskToStatus(task, 'inbox').then(() => {
|
|
414
698
|
if (selectedTaskIndex >= currentTasks.length - 1) {
|
|
@@ -439,27 +723,26 @@ function AppContent() {
|
|
|
439
723
|
};
|
|
440
724
|
// Turso 接続情報を取得
|
|
441
725
|
const tursoEnabled = isTursoEnabled();
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
try {
|
|
446
|
-
return new URL(config.url).host;
|
|
447
|
-
}
|
|
448
|
-
catch {
|
|
449
|
-
return config.url;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
return '';
|
|
453
|
-
})() : '';
|
|
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) => {
|
|
726
|
+
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}` }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: theme.name === 'modern'
|
|
727
|
+
? (tursoEnabled ? ' ☁️ turso' : ' 💾 local')
|
|
728
|
+
: (tursoEnabled ? ' [DB]turso' : ' [DB]local') })] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
|
|
455
729
|
const isActive = index === currentListIndex && mode !== 'project-detail';
|
|
456
730
|
const count = tasks[tab].length;
|
|
457
731
|
const label = `${index + 1}:${getTabLabel(tab)}(${count})`;
|
|
458
732
|
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:
|
|
733
|
+
}) }), 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(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus, ": "] }), _jsxs(Text, { color: theme.colors.accent, 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) => {
|
|
734
|
+
const isSelected = index === selectedCommentIndex && mode === 'task-detail';
|
|
735
|
+
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));
|
|
736
|
+
})) }), 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
737
|
const parentProject = getParentProject(task.parentId);
|
|
461
|
-
|
|
462
|
-
|
|
738
|
+
const progress = currentTab === 'projects' ? projectProgress[task.id] : undefined;
|
|
739
|
+
return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress }, task.id));
|
|
740
|
+
})) })), (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
741
|
? `${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' })] })),
|
|
742
|
+
: 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 })), mode === 'confirm-delete' && taskToDelete && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })), 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: [
|
|
743
|
+
{ key: 'i', label: i18n.tui.keyBar.comment },
|
|
744
|
+
{ key: 'd', label: i18n.tui.keyBar.delete },
|
|
745
|
+
{ key: 'P', label: i18n.tui.keyBar.project },
|
|
746
|
+
{ key: 'b', label: i18n.tui.keyBar.back },
|
|
747
|
+
] })) : (_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
748
|
}
|
|
@@ -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
|
+
}
|