floq 0.3.1 → 0.5.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 +12 -1
- package/README.md +12 -1
- package/dist/cli.js +24 -0
- package/dist/commands/add.d.ts +1 -0
- package/dist/commands/add.js +2 -0
- package/dist/commands/context.d.ts +3 -0
- package/dist/commands/context.js +36 -0
- package/dist/commands/list.js +17 -5
- package/dist/config.d.ts +4 -0
- package/dist/config.js +25 -0
- package/dist/db/index.js +16 -0
- package/dist/db/schema.d.ts +19 -0
- package/dist/db/schema.js +1 -0
- package/dist/i18n/en.d.ts +44 -0
- package/dist/i18n/en.js +26 -0
- package/dist/i18n/ja.js +26 -0
- package/dist/ui/App.js +187 -13
- package/dist/ui/components/KanbanBoard.js +173 -16
- package/dist/ui/components/SearchResults.d.ts +2 -1
- package/dist/ui/components/SearchResults.js +22 -2
- package/dist/ui/components/TaskItem.js +1 -1
- package/dist/ui/history/commands/SetContextCommand.d.ts +20 -0
- package/dist/ui/history/commands/SetContextCommand.js +37 -0
- package/dist/ui/history/commands/index.d.ts +1 -0
- package/dist/ui/history/commands/index.js +1 -0
- package/dist/ui/history/index.d.ts +1 -1
- package/dist/ui/history/index.js +1 -1
- package/package.json +1 -1
package/dist/ui/App.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
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';
|
|
5
|
-
import { eq, and } from 'drizzle-orm';
|
|
5
|
+
import { eq, and, gte } from 'drizzle-orm';
|
|
6
6
|
import { v4 as uuidv4 } from 'uuid';
|
|
7
7
|
import { TaskItem } from './components/TaskItem.js';
|
|
8
8
|
import { HelpModal } from './components/HelpModal.js';
|
|
@@ -16,10 +16,10 @@ import { LanguageSelector } from './LanguageSelector.js';
|
|
|
16
16
|
import { getDb, schema } from '../db/index.js';
|
|
17
17
|
import { t, fmt } from '../i18n/index.js';
|
|
18
18
|
import { ThemeProvider, useTheme } from './theme/index.js';
|
|
19
|
-
import { getThemeName, getViewMode, setThemeName, setViewMode, setLocale, isTursoEnabled } from '../config.js';
|
|
19
|
+
import { getThemeName, getViewMode, setThemeName, setViewMode, setLocale, isTursoEnabled, getContexts, addContext } from '../config.js';
|
|
20
20
|
import { KanbanBoard } from './components/KanbanBoard.js';
|
|
21
21
|
import { VERSION } from '../version.js';
|
|
22
|
-
import { HistoryProvider, useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, } from './history/index.js';
|
|
22
|
+
import { HistoryProvider, useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from './history/index.js';
|
|
23
23
|
const TABS = ['inbox', 'next', 'waiting', 'someday', 'projects', 'done'];
|
|
24
24
|
export function App() {
|
|
25
25
|
const [themeName, setThemeNameState] = useState(getThemeName);
|
|
@@ -87,6 +87,10 @@ function AppContent({ onOpenSettings }) {
|
|
|
87
87
|
const [searchQuery, setSearchQuery] = useState('');
|
|
88
88
|
const [searchResults, setSearchResults] = useState([]);
|
|
89
89
|
const [searchResultIndex, setSearchResultIndex] = useState(0);
|
|
90
|
+
// Context filter state
|
|
91
|
+
const [contextFilter, setContextFilter] = useState(null); // null = all, '' = no context, string = specific context
|
|
92
|
+
const [contextSelectIndex, setContextSelectIndex] = useState(0);
|
|
93
|
+
const [availableContexts, setAvailableContexts] = useState([]);
|
|
90
94
|
const i18n = t();
|
|
91
95
|
const loadTasks = useCallback(async () => {
|
|
92
96
|
const db = getDb();
|
|
@@ -101,12 +105,35 @@ function AppContent({ onOpenSettings }) {
|
|
|
101
105
|
// Load all tasks (including project children) by status
|
|
102
106
|
const statusList = ['inbox', 'next', 'waiting', 'someday', 'done'];
|
|
103
107
|
for (const status of statusList) {
|
|
104
|
-
|
|
108
|
+
// For done tasks, only show those completed in the last week
|
|
109
|
+
const oneWeekAgo = new Date();
|
|
110
|
+
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
|
111
|
+
const conditions = [
|
|
112
|
+
eq(schema.tasks.status, status),
|
|
113
|
+
eq(schema.tasks.isProject, false),
|
|
114
|
+
];
|
|
115
|
+
// Filter done tasks to last week only
|
|
116
|
+
if (status === 'done') {
|
|
117
|
+
conditions.push(gte(schema.tasks.updatedAt, oneWeekAgo));
|
|
118
|
+
}
|
|
119
|
+
let allTasks = await db
|
|
105
120
|
.select()
|
|
106
121
|
.from(schema.tasks)
|
|
107
|
-
.where(and(
|
|
122
|
+
.where(and(...conditions));
|
|
123
|
+
// Apply context filter
|
|
124
|
+
if (contextFilter !== null) {
|
|
125
|
+
if (contextFilter === '') {
|
|
126
|
+
// Filter to tasks with no context
|
|
127
|
+
allTasks = allTasks.filter(t => !t.context);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// Filter to specific context
|
|
131
|
+
allTasks = allTasks.filter(t => t.context === contextFilter);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
newTasks[status] = allTasks;
|
|
108
135
|
}
|
|
109
|
-
// Load projects (isProject = true, not done)
|
|
136
|
+
// Load projects (isProject = true, not done) - projects don't get context filtered
|
|
110
137
|
newTasks.projects = await db
|
|
111
138
|
.select()
|
|
112
139
|
.from(schema.tasks)
|
|
@@ -124,7 +151,8 @@ function AppContent({ onOpenSettings }) {
|
|
|
124
151
|
}
|
|
125
152
|
setProjectProgress(progress);
|
|
126
153
|
setTasks(newTasks);
|
|
127
|
-
|
|
154
|
+
setAvailableContexts(getContexts());
|
|
155
|
+
}, [contextFilter]);
|
|
128
156
|
// Get parent project for a task
|
|
129
157
|
const getParentProject = (parentId) => {
|
|
130
158
|
if (!parentId)
|
|
@@ -188,7 +216,7 @@ function AppContent({ onOpenSettings }) {
|
|
|
188
216
|
setMode('normal');
|
|
189
217
|
}
|
|
190
218
|
}, [tasks]);
|
|
191
|
-
const addTask = useCallback(async (title, parentId) => {
|
|
219
|
+
const addTask = useCallback(async (title, parentId, context) => {
|
|
192
220
|
if (!title.trim())
|
|
193
221
|
return;
|
|
194
222
|
const now = new Date();
|
|
@@ -199,6 +227,7 @@ function AppContent({ onOpenSettings }) {
|
|
|
199
227
|
title: title.trim(),
|
|
200
228
|
status: parentId ? 'next' : 'inbox',
|
|
201
229
|
parentId: parentId || null,
|
|
230
|
+
context: context || null,
|
|
202
231
|
createdAt: now,
|
|
203
232
|
updatedAt: now,
|
|
204
233
|
},
|
|
@@ -258,18 +287,36 @@ function AppContent({ onOpenSettings }) {
|
|
|
258
287
|
setSearchResultIndex(0);
|
|
259
288
|
return;
|
|
260
289
|
}
|
|
290
|
+
// Handle add-context mode submit
|
|
291
|
+
if (mode === 'add-context') {
|
|
292
|
+
if (value.trim()) {
|
|
293
|
+
const newContext = value.trim().toLowerCase().replace(/^@/, '');
|
|
294
|
+
addContext(newContext);
|
|
295
|
+
setAvailableContexts(getContexts());
|
|
296
|
+
// Set the new context on the current task
|
|
297
|
+
if (currentTasks.length > 0) {
|
|
298
|
+
const task = currentTasks[selectedTaskIndex];
|
|
299
|
+
await setTaskContext(task, newContext);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
setInputValue('');
|
|
303
|
+
setContextSelectIndex(0);
|
|
304
|
+
setMode('normal');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
261
307
|
if (value.trim()) {
|
|
262
308
|
if (mode === 'add-comment' && selectedTask) {
|
|
263
309
|
await addCommentToTask(selectedTask, value);
|
|
264
310
|
setMode('task-detail');
|
|
265
311
|
}
|
|
266
312
|
else if (mode === 'add-to-project' && selectedProject) {
|
|
267
|
-
await addTask(value, selectedProject.id);
|
|
313
|
+
await addTask(value, selectedProject.id, contextFilter && contextFilter !== '' ? contextFilter : null);
|
|
268
314
|
await loadProjectTasks(selectedProject.id);
|
|
269
315
|
setMode('project-detail');
|
|
270
316
|
}
|
|
271
317
|
else {
|
|
272
|
-
|
|
318
|
+
// Pass contextFilter when adding a task, so it inherits the current filter context
|
|
319
|
+
await addTask(value, undefined, contextFilter && contextFilter !== '' ? contextFilter : null);
|
|
273
320
|
setMode('normal');
|
|
274
321
|
}
|
|
275
322
|
}
|
|
@@ -343,6 +390,20 @@ function AppContent({ onOpenSettings }) {
|
|
|
343
390
|
setMessage(fmt(i18n.tui.madeProject || 'Made project: {title}', { title: task.title }));
|
|
344
391
|
await loadTasks();
|
|
345
392
|
}, [i18n.tui.madeProject, loadTasks, history]);
|
|
393
|
+
const setTaskContext = useCallback(async (task, context) => {
|
|
394
|
+
const description = context
|
|
395
|
+
? fmt(i18n.tui.context?.contextSet || 'Set context @{context} for "{title}"', { context, title: task.title })
|
|
396
|
+
: fmt(i18n.tui.context?.contextCleared || 'Cleared context for "{title}"', { title: task.title });
|
|
397
|
+
const command = new SetContextCommand({
|
|
398
|
+
taskId: task.id,
|
|
399
|
+
fromContext: task.context,
|
|
400
|
+
toContext: context,
|
|
401
|
+
description,
|
|
402
|
+
});
|
|
403
|
+
await history.execute(command);
|
|
404
|
+
setMessage(description);
|
|
405
|
+
await loadTasks();
|
|
406
|
+
}, [i18n.tui.context, loadTasks, history]);
|
|
346
407
|
const deleteTask = useCallback(async (task) => {
|
|
347
408
|
const command = new DeleteTaskCommand({
|
|
348
409
|
task,
|
|
@@ -519,6 +580,87 @@ function AppContent({ onOpenSettings }) {
|
|
|
519
580
|
}
|
|
520
581
|
return;
|
|
521
582
|
}
|
|
583
|
+
// Handle context-filter mode
|
|
584
|
+
if (mode === 'context-filter') {
|
|
585
|
+
if (key.escape) {
|
|
586
|
+
setContextSelectIndex(0);
|
|
587
|
+
setMode('normal');
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
// Navigate context options (All, No context, then each context)
|
|
591
|
+
const contextOptions = ['all', 'none', ...availableContexts];
|
|
592
|
+
if (key.upArrow || input === 'k') {
|
|
593
|
+
setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (key.downArrow || input === 'j') {
|
|
597
|
+
setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
// Select context with Enter
|
|
601
|
+
if (key.return) {
|
|
602
|
+
const selected = contextOptions[contextSelectIndex];
|
|
603
|
+
if (selected === 'all') {
|
|
604
|
+
setContextFilter(null);
|
|
605
|
+
}
|
|
606
|
+
else if (selected === 'none') {
|
|
607
|
+
setContextFilter('');
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
setContextFilter(selected);
|
|
611
|
+
}
|
|
612
|
+
setContextSelectIndex(0);
|
|
613
|
+
setMode('normal');
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
// Handle set-context mode
|
|
619
|
+
if (mode === 'set-context') {
|
|
620
|
+
if (key.escape) {
|
|
621
|
+
setContextSelectIndex(0);
|
|
622
|
+
setMode('normal');
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
// Navigate context options (Clear, each context, then + New)
|
|
626
|
+
const contextOptions = ['clear', ...availableContexts, 'new'];
|
|
627
|
+
if (key.upArrow || input === 'k') {
|
|
628
|
+
setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (key.downArrow || input === 'j') {
|
|
632
|
+
setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
// Select context with Enter
|
|
636
|
+
if (key.return && currentTasks.length > 0) {
|
|
637
|
+
const selected = contextOptions[contextSelectIndex];
|
|
638
|
+
if (selected === 'new') {
|
|
639
|
+
setMode('add-context');
|
|
640
|
+
setInputValue('');
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const task = currentTasks[selectedTaskIndex];
|
|
644
|
+
if (selected === 'clear') {
|
|
645
|
+
setTaskContext(task, null);
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
setTaskContext(task, selected);
|
|
649
|
+
}
|
|
650
|
+
setContextSelectIndex(0);
|
|
651
|
+
setMode('normal');
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
// Handle add-context mode
|
|
657
|
+
if (mode === 'add-context') {
|
|
658
|
+
if (key.escape) {
|
|
659
|
+
setInputValue('');
|
|
660
|
+
setMode('set-context');
|
|
661
|
+
}
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
522
664
|
// Handle project-detail mode
|
|
523
665
|
if (mode === 'project-detail') {
|
|
524
666
|
if (key.escape || key.backspace || input === 'b') {
|
|
@@ -622,6 +764,18 @@ function AppContent({ onOpenSettings }) {
|
|
|
622
764
|
setSearchResultIndex(0);
|
|
623
765
|
return;
|
|
624
766
|
}
|
|
767
|
+
// Context filter mode
|
|
768
|
+
if (input === '@') {
|
|
769
|
+
setContextSelectIndex(0);
|
|
770
|
+
setMode('context-filter');
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
// Set context mode (c key)
|
|
774
|
+
if (input === 'c' && currentTasks.length > 0 && currentTab !== 'projects') {
|
|
775
|
+
setContextSelectIndex(0);
|
|
776
|
+
setMode('set-context');
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
625
779
|
// Settings: Theme selector
|
|
626
780
|
if (input === 'T') {
|
|
627
781
|
onOpenSettings('theme-select');
|
|
@@ -841,12 +995,12 @@ function AppContent({ onOpenSettings }) {
|
|
|
841
995
|
const tursoEnabled = isTursoEnabled();
|
|
842
996
|
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'
|
|
843
997
|
? (tursoEnabled ? ' ☁️ turso' : ' 💾 local')
|
|
844
|
-
: (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') })] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
|
|
998
|
+
: (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? (i18n.tui.context?.none || 'none') : contextFilter] }))] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
|
|
845
999
|
const isActive = index === currentListIndex && mode !== 'project-detail';
|
|
846
1000
|
const count = tasks[tab].length;
|
|
847
1001
|
const label = `${index + 1}:${getTabLabel(tab)}(${count})`;
|
|
848
1002
|
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));
|
|
849
|
-
}) }), 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) => {
|
|
1003
|
+
}) }), 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})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] })] }), _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) => {
|
|
850
1004
|
const isSelected = index === selectedCommentIndex && mode === 'task-detail';
|
|
851
1005
|
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));
|
|
852
1006
|
})) }), 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) => {
|
|
@@ -855,7 +1009,27 @@ function AppContent({ onOpenSettings }) {
|
|
|
855
1009
|
return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress }, task.id));
|
|
856
1010
|
})) })), (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
|
|
857
1011
|
? `${i18n.tui.newTask}[${selectedProject.title}] `
|
|
858
|
-
: 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 === '
|
|
1012
|
+
: 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 === 'context-filter' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.filter || 'Filter by context' }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
|
|
1013
|
+
const label = ctx === 'all'
|
|
1014
|
+
? (i18n.tui.context?.all || 'All')
|
|
1015
|
+
: ctx === 'none'
|
|
1016
|
+
? (i18n.tui.context?.none || 'No context')
|
|
1017
|
+
: `@${ctx}`;
|
|
1018
|
+
const isActive = (ctx === 'all' && contextFilter === null) ||
|
|
1019
|
+
(ctx === 'none' && contextFilter === '') ||
|
|
1020
|
+
(ctx !== 'all' && ctx !== 'none' && contextFilter === ctx);
|
|
1021
|
+
return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, label, isActive && ' *'] }, ctx));
|
|
1022
|
+
}) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.filterHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), mode === 'set-context' && currentTasks.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.setContext || 'Set context', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: ['clear', ...availableContexts, 'new'].map((ctx, index) => {
|
|
1023
|
+
const label = ctx === 'clear'
|
|
1024
|
+
? (i18n.tui.context?.none || 'No context')
|
|
1025
|
+
: ctx === 'new'
|
|
1026
|
+
? (i18n.tui.context?.addNew || '+ New context')
|
|
1027
|
+
: `@${ctx}`;
|
|
1028
|
+
const currentContext = currentTasks[selectedTaskIndex]?.context;
|
|
1029
|
+
const isActive = (ctx === 'clear' && !currentContext) ||
|
|
1030
|
+
(ctx !== 'clear' && ctx !== 'new' && currentContext === ctx);
|
|
1031
|
+
return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, label, isActive && ' *'] }, ctx));
|
|
1032
|
+
}) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.setContextHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), mode === 'add-context' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.newContext || 'New context: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.context?.newContextPlaceholder || 'Enter context name...' }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), 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: [
|
|
859
1033
|
{ key: 'i', label: i18n.tui.keyBar.comment },
|
|
860
1034
|
{ key: 'd', label: i18n.tui.keyBar.delete },
|
|
861
1035
|
{ key: 'P', label: i18n.tui.keyBar.project },
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
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';
|
|
5
|
-
import { eq, and, inArray } from 'drizzle-orm';
|
|
5
|
+
import { eq, and, inArray, gte } from 'drizzle-orm';
|
|
6
6
|
import { v4 as uuidv4 } from 'uuid';
|
|
7
7
|
import { KanbanColumn } from './KanbanColumn.js';
|
|
8
8
|
import { HelpModal } from './HelpModal.js';
|
|
@@ -12,9 +12,9 @@ import { SearchResults } from './SearchResults.js';
|
|
|
12
12
|
import { getDb, schema } from '../../db/index.js';
|
|
13
13
|
import { t, fmt } from '../../i18n/index.js';
|
|
14
14
|
import { useTheme } from '../theme/index.js';
|
|
15
|
-
import { isTursoEnabled } from '../../config.js';
|
|
15
|
+
import { isTursoEnabled, getContexts, addContext } from '../../config.js';
|
|
16
16
|
import { VERSION } from '../../version.js';
|
|
17
|
-
import { useHistory, CreateTaskCommand, MoveTaskCommand, LinkTaskCommand, CreateCommentCommand, DeleteCommentCommand, } from '../history/index.js';
|
|
17
|
+
import { useHistory, CreateTaskCommand, MoveTaskCommand, LinkTaskCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from '../history/index.js';
|
|
18
18
|
const COLUMNS = ['todo', 'doing', 'done'];
|
|
19
19
|
export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
20
20
|
const theme = useTheme();
|
|
@@ -43,6 +43,10 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
43
43
|
const [searchQuery, setSearchQuery] = useState('');
|
|
44
44
|
const [searchResults, setSearchResults] = useState([]);
|
|
45
45
|
const [searchResultIndex, setSearchResultIndex] = useState(0);
|
|
46
|
+
// Context filter state
|
|
47
|
+
const [contextFilter, setContextFilter] = useState(null);
|
|
48
|
+
const [contextSelectIndex, setContextSelectIndex] = useState(0);
|
|
49
|
+
const [availableContexts, setAvailableContexts] = useState([]);
|
|
46
50
|
const i18n = t();
|
|
47
51
|
// Status mapping:
|
|
48
52
|
// TODO = inbox + someday
|
|
@@ -50,33 +54,44 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
50
54
|
// Done = done
|
|
51
55
|
const loadTasks = useCallback(async () => {
|
|
52
56
|
const db = getDb();
|
|
57
|
+
// Apply context filter helper
|
|
58
|
+
const filterByContext = (taskList) => {
|
|
59
|
+
if (contextFilter === null)
|
|
60
|
+
return taskList;
|
|
61
|
+
if (contextFilter === '')
|
|
62
|
+
return taskList.filter(t => !t.context);
|
|
63
|
+
return taskList.filter(t => t.context === contextFilter);
|
|
64
|
+
};
|
|
53
65
|
// TODO: inbox + someday (non-project tasks)
|
|
54
|
-
|
|
66
|
+
let todoTasks = await db
|
|
55
67
|
.select()
|
|
56
68
|
.from(schema.tasks)
|
|
57
69
|
.where(and(inArray(schema.tasks.status, ['inbox', 'someday']), eq(schema.tasks.isProject, false)));
|
|
58
70
|
// Doing: next + waiting (non-project tasks)
|
|
59
|
-
|
|
71
|
+
let doingTasks = await db
|
|
60
72
|
.select()
|
|
61
73
|
.from(schema.tasks)
|
|
62
74
|
.where(and(inArray(schema.tasks.status, ['next', 'waiting']), eq(schema.tasks.isProject, false)));
|
|
63
|
-
// Done: done (non-project tasks)
|
|
64
|
-
const
|
|
75
|
+
// Done: done (non-project tasks) - only show last week
|
|
76
|
+
const oneWeekAgo = new Date();
|
|
77
|
+
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
|
78
|
+
let doneTasks = await db
|
|
65
79
|
.select()
|
|
66
80
|
.from(schema.tasks)
|
|
67
|
-
.where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false)));
|
|
81
|
+
.where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false), gte(schema.tasks.updatedAt, oneWeekAgo)));
|
|
68
82
|
// Load projects for linking
|
|
69
83
|
const projectTasks = await db
|
|
70
84
|
.select()
|
|
71
85
|
.from(schema.tasks)
|
|
72
86
|
.where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.status, 'next')));
|
|
73
87
|
setTasks({
|
|
74
|
-
todo: todoTasks,
|
|
75
|
-
doing: doingTasks,
|
|
76
|
-
done: doneTasks,
|
|
88
|
+
todo: filterByContext(todoTasks),
|
|
89
|
+
doing: filterByContext(doingTasks),
|
|
90
|
+
done: filterByContext(doneTasks),
|
|
77
91
|
});
|
|
78
92
|
setProjects(projectTasks);
|
|
79
|
-
|
|
93
|
+
setAvailableContexts(getContexts());
|
|
94
|
+
}, [contextFilter]);
|
|
80
95
|
useEffect(() => {
|
|
81
96
|
loadTasks();
|
|
82
97
|
}, [loadTasks]);
|
|
@@ -176,7 +191,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
176
191
|
setMode('normal');
|
|
177
192
|
}
|
|
178
193
|
}, [tasks]);
|
|
179
|
-
const addTask = useCallback(async (title) => {
|
|
194
|
+
const addTask = useCallback(async (title, context) => {
|
|
180
195
|
if (!title.trim())
|
|
181
196
|
return;
|
|
182
197
|
const now = new Date();
|
|
@@ -186,6 +201,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
186
201
|
id: taskId,
|
|
187
202
|
title: title.trim(),
|
|
188
203
|
status: 'inbox', // New tasks go to inbox (which maps to TODO)
|
|
204
|
+
context: context || null,
|
|
189
205
|
createdAt: now,
|
|
190
206
|
updatedAt: now,
|
|
191
207
|
},
|
|
@@ -218,8 +234,26 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
218
234
|
setSearchResultIndex(0);
|
|
219
235
|
return;
|
|
220
236
|
}
|
|
237
|
+
// Handle add-context mode submit
|
|
238
|
+
if (mode === 'add-context') {
|
|
239
|
+
if (value.trim()) {
|
|
240
|
+
const newContext = value.trim().toLowerCase().replace(/^@/, '');
|
|
241
|
+
addContext(newContext);
|
|
242
|
+
setAvailableContexts(getContexts());
|
|
243
|
+
// Set the new context on the current task
|
|
244
|
+
if (currentTasks.length > 0) {
|
|
245
|
+
const task = currentTasks[selectedTaskIndex];
|
|
246
|
+
await setTaskContext(task, newContext);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
setInputValue('');
|
|
250
|
+
setContextSelectIndex(0);
|
|
251
|
+
setMode('normal');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
221
254
|
if (value.trim()) {
|
|
222
|
-
|
|
255
|
+
// Pass contextFilter when adding a task, so it inherits the current filter context
|
|
256
|
+
await addTask(value, contextFilter && contextFilter !== '' ? contextFilter : null);
|
|
223
257
|
}
|
|
224
258
|
setInputValue('');
|
|
225
259
|
setMode('normal');
|
|
@@ -291,6 +325,20 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
291
325
|
setMessage(fmt(i18n.tui.completed, { title: task.title }));
|
|
292
326
|
await loadTasks();
|
|
293
327
|
}, [i18n.tui.completed, loadTasks, history]);
|
|
328
|
+
const setTaskContext = useCallback(async (task, context) => {
|
|
329
|
+
const description = context
|
|
330
|
+
? fmt(i18n.tui.context?.contextSet || 'Set context @{context} for "{title}"', { context, title: task.title })
|
|
331
|
+
: fmt(i18n.tui.context?.contextCleared || 'Cleared context for "{title}"', { title: task.title });
|
|
332
|
+
const command = new SetContextCommand({
|
|
333
|
+
taskId: task.id,
|
|
334
|
+
fromContext: task.context,
|
|
335
|
+
toContext: context,
|
|
336
|
+
description,
|
|
337
|
+
});
|
|
338
|
+
await history.execute(command);
|
|
339
|
+
setMessage(description);
|
|
340
|
+
await loadTasks();
|
|
341
|
+
}, [i18n.tui.context, loadTasks, history]);
|
|
294
342
|
const getColumnLabel = (column) => {
|
|
295
343
|
return i18n.kanban[column];
|
|
296
344
|
};
|
|
@@ -401,6 +449,83 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
401
449
|
}
|
|
402
450
|
return;
|
|
403
451
|
}
|
|
452
|
+
// Handle context-filter mode
|
|
453
|
+
if (mode === 'context-filter') {
|
|
454
|
+
if (key.escape) {
|
|
455
|
+
setContextSelectIndex(0);
|
|
456
|
+
setMode('normal');
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const contextOptions = ['all', 'none', ...availableContexts];
|
|
460
|
+
if (key.upArrow || input === 'k') {
|
|
461
|
+
setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (key.downArrow || input === 'j') {
|
|
465
|
+
setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (key.return) {
|
|
469
|
+
const selected = contextOptions[contextSelectIndex];
|
|
470
|
+
if (selected === 'all') {
|
|
471
|
+
setContextFilter(null);
|
|
472
|
+
}
|
|
473
|
+
else if (selected === 'none') {
|
|
474
|
+
setContextFilter('');
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
setContextFilter(selected);
|
|
478
|
+
}
|
|
479
|
+
setContextSelectIndex(0);
|
|
480
|
+
setMode('normal');
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
// Handle set-context mode
|
|
486
|
+
if (mode === 'set-context') {
|
|
487
|
+
if (key.escape) {
|
|
488
|
+
setContextSelectIndex(0);
|
|
489
|
+
setMode('normal');
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const contextOptions = ['clear', ...availableContexts, 'new'];
|
|
493
|
+
if (key.upArrow || input === 'k') {
|
|
494
|
+
setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (key.downArrow || input === 'j') {
|
|
498
|
+
setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (key.return && currentTasks.length > 0) {
|
|
502
|
+
const selected = contextOptions[contextSelectIndex];
|
|
503
|
+
if (selected === 'new') {
|
|
504
|
+
setMode('add-context');
|
|
505
|
+
setInputValue('');
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const task = currentTasks[selectedTaskIndex];
|
|
509
|
+
if (selected === 'clear') {
|
|
510
|
+
setTaskContext(task, null);
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
setTaskContext(task, selected);
|
|
514
|
+
}
|
|
515
|
+
setContextSelectIndex(0);
|
|
516
|
+
setMode('normal');
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
// Handle add-context mode
|
|
522
|
+
if (mode === 'add-context') {
|
|
523
|
+
if (key.escape) {
|
|
524
|
+
setInputValue('');
|
|
525
|
+
setMode('set-context');
|
|
526
|
+
}
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
404
529
|
// Clear message on any input
|
|
405
530
|
if (message) {
|
|
406
531
|
setMessage(null);
|
|
@@ -418,6 +543,18 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
418
543
|
setSearchResultIndex(0);
|
|
419
544
|
return;
|
|
420
545
|
}
|
|
546
|
+
// Context filter mode
|
|
547
|
+
if (input === '@') {
|
|
548
|
+
setContextSelectIndex(0);
|
|
549
|
+
setMode('context-filter');
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
// Set context mode (c key)
|
|
553
|
+
if (input === 'c' && currentTasks.length > 0) {
|
|
554
|
+
setContextSelectIndex(0);
|
|
555
|
+
setMode('set-context');
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
421
558
|
// Settings: Theme selector
|
|
422
559
|
if (input === 'T' && onOpenSettings) {
|
|
423
560
|
onOpenSettings('theme-select');
|
|
@@ -575,10 +712,30 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
575
712
|
const tursoEnabled = isTursoEnabled();
|
|
576
713
|
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.accent, children: " [KANBAN]" }), _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'
|
|
577
714
|
? (tursoEnabled ? ' ☁️ turso' : ' 💾 local')
|
|
578
|
-
: (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') })] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), (mode === 'task-detail' || mode === 'add-comment' || mode === 'select-project') && 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.
|
|
715
|
+
: (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? (i18n.tui.context?.none || 'none') : contextFilter] }))] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), (mode === 'task-detail' || mode === 'add-comment' || mode === 'select-project') && 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})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), selectedTask.dueDate && (_jsxs(Text, { color: theme.colors.textMuted, children: ["Due: ", selectedTask.dueDate.toLocaleDateString()] }))] }), _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) => {
|
|
579
716
|
const isSelected = index === selectedCommentIndex && mode === 'task-detail';
|
|
580
717
|
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));
|
|
581
|
-
})) }), 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 === 'select-project' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", selectedTask.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: 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(_Fragment, { children: searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery
|
|
718
|
+
})) }), 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 === 'select-project' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", selectedTask.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: 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(_Fragment, { children: searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery, viewMode: "kanban" })) })) : mode === 'context-filter' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.filter || 'Filter by context' }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
|
|
719
|
+
const label = ctx === 'all'
|
|
720
|
+
? (i18n.tui.context?.all || 'All')
|
|
721
|
+
: ctx === 'none'
|
|
722
|
+
? (i18n.tui.context?.none || 'No context')
|
|
723
|
+
: `@${ctx}`;
|
|
724
|
+
const isActive = (ctx === 'all' && contextFilter === null) ||
|
|
725
|
+
(ctx === 'none' && contextFilter === '') ||
|
|
726
|
+
(ctx !== 'all' && ctx !== 'none' && contextFilter === ctx);
|
|
727
|
+
return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, label, isActive && ' *'] }, ctx));
|
|
728
|
+
}) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.filterHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })) : mode === 'set-context' && currentTasks.length > 0 ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.setContext || 'Set context', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: ['clear', ...availableContexts, 'new'].map((ctx, index) => {
|
|
729
|
+
const label = ctx === 'clear'
|
|
730
|
+
? (i18n.tui.context?.none || 'No context')
|
|
731
|
+
: ctx === 'new'
|
|
732
|
+
? (i18n.tui.context?.addNew || '+ New context')
|
|
733
|
+
: `@${ctx}`;
|
|
734
|
+
const currentContext = currentTasks[selectedTaskIndex]?.context;
|
|
735
|
+
const isActive = (ctx === 'clear' && !currentContext) ||
|
|
736
|
+
(ctx !== 'clear' && ctx !== 'new' && currentContext === ctx);
|
|
737
|
+
return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, label, isActive && ' *'] }, ctx));
|
|
738
|
+
}) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.setContextHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })) : mode === 'add-context' ? (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.newContext || 'New context: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.context?.newContextPlaceholder || 'Enter context name...' }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })) : (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: COLUMNS.map((column, index) => (_jsx(Box, { flexGrow: 1, flexBasis: 0, marginRight: index < 2 ? 1 : 0, children: _jsxs(Text, { color: currentColumnIndex === index ? theme.colors.textHighlight : theme.colors.textMuted, children: [index + 1, ":"] }) }, column))) }), _jsx(Box, { flexDirection: "row", children: COLUMNS.map((column, index) => (_jsx(KanbanColumn, { title: getColumnLabel(column), tasks: tasks[column], isActive: index === currentColumnIndex, selectedTaskIndex: selectedTaskIndices[column], columnIndex: index }, column))) })] })), mode === 'add' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: 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 === '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 === 'select-project' ? (_jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })) : (mode === 'task-detail' || mode === 'add-comment') ? (theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, { keys: [
|
|
582
739
|
{ key: 'i', label: i18n.tui.keyBar.comment },
|
|
583
740
|
{ key: 'd', label: i18n.tui.keyBar.delete },
|
|
584
741
|
{ key: 'P', label: i18n.tui.keyBar.project },
|