floq 1.3.3 → 1.4.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 +8 -0
- package/README.md +8 -0
- package/dist/cli.js +10 -0
- package/dist/commands/insights.d.ts +1 -0
- package/dist/commands/insights.js +283 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +6 -0
- package/dist/db/index.js +24 -0
- package/dist/db/schema.d.ts +37 -0
- package/dist/db/schema.js +2 -0
- package/dist/i18n/en.d.ts +82 -0
- package/dist/i18n/en.js +57 -5
- package/dist/i18n/ja.js +57 -5
- package/dist/ui/App.js +110 -7
- package/dist/ui/components/GtdDQ.js +139 -12
- package/dist/ui/components/GtdMario.js +135 -11
- package/dist/ui/components/HelpModal.js +8 -0
- package/dist/ui/components/InsightsModal.d.ts +6 -0
- package/dist/ui/components/InsightsModal.js +272 -0
- package/dist/ui/components/KanbanBoard.js +119 -11
- package/dist/ui/components/KanbanColumn.js +7 -2
- package/dist/ui/components/KanbanDQ.js +120 -11
- package/dist/ui/components/KanbanMario.js +125 -11
- package/dist/ui/components/TaskItem.js +7 -2
- package/dist/ui/history/commands/SetEffortCommand.d.ts +20 -0
- package/dist/ui/history/commands/SetEffortCommand.js +37 -0
- package/dist/ui/history/commands/SetFocusCommand.d.ts +20 -0
- package/dist/ui/history/commands/SetFocusCommand.js +37 -0
- package/dist/ui/history/commands/index.d.ts +2 -0
- package/dist/ui/history/commands/index.js +2 -0
- package/dist/ui/history/index.d.ts +1 -1
- package/dist/ui/history/index.js +1 -1
- package/package.json +1 -1
|
@@ -28,6 +28,9 @@ export function HelpModal({ onClose, isKanban = false }) {
|
|
|
28
28
|
{ type: 'key', key: 'd', value: kHelp.completeTask },
|
|
29
29
|
{ type: 'key', key: 'm', value: kHelp.moveRight },
|
|
30
30
|
{ type: 'key', key: 'BS', value: kHelp.moveLeft },
|
|
31
|
+
{ type: 'key', key: 'g', value: kHelp.toggleFocus || 'Toggle focus' },
|
|
32
|
+
{ type: 'key', key: 'G', value: kHelp.toggleFocusFilter || 'Toggle focus filter' },
|
|
33
|
+
{ type: 'key', key: 'E', value: kHelp.setEffort || 'Set effort size' },
|
|
31
34
|
{ type: 'key', key: 'u', value: 'Undo' },
|
|
32
35
|
{ type: 'key', key: 'Ctrl+r', value: 'Redo' },
|
|
33
36
|
{ type: 'header', value: kHelp.settings },
|
|
@@ -36,6 +39,7 @@ export function HelpModal({ onClose, isKanban = false }) {
|
|
|
36
39
|
{ type: 'key', key: 'L', value: kHelp.changeLanguage },
|
|
37
40
|
{ type: 'header', value: kHelp.other },
|
|
38
41
|
{ type: 'key', key: '/', value: kHelp.searchTasks },
|
|
42
|
+
{ type: 'key', key: 'I', value: help.showInsights || 'Show insights' },
|
|
39
43
|
{ type: 'key', key: '?', value: kHelp.showHelp },
|
|
40
44
|
{ type: 'key', key: 'q', value: kHelp.quit },
|
|
41
45
|
];
|
|
@@ -53,6 +57,9 @@ export function HelpModal({ onClose, isKanban = false }) {
|
|
|
53
57
|
{ type: 'key', key: 'w', value: help.moveToWaiting },
|
|
54
58
|
{ type: 'key', key: 'i', value: help.moveToInbox },
|
|
55
59
|
{ type: 'key', key: 'r', value: help.refresh },
|
|
60
|
+
{ type: 'key', key: 'g', value: help.toggleFocus || 'Toggle focus' },
|
|
61
|
+
{ type: 'key', key: 'G', value: help.toggleFocusFilter || 'Toggle focus filter' },
|
|
62
|
+
{ type: 'key', key: 'E', value: help.setEffort || 'Set effort size' },
|
|
56
63
|
{ type: 'key', key: 'u', value: help.undo },
|
|
57
64
|
{ type: 'key', key: 'Ctrl+r', value: help.redo },
|
|
58
65
|
{ type: 'header', value: help.projects },
|
|
@@ -72,6 +79,7 @@ export function HelpModal({ onClose, isKanban = false }) {
|
|
|
72
79
|
{ type: 'header', value: help.other },
|
|
73
80
|
{ type: 'key', key: '/', value: help.searchTasks },
|
|
74
81
|
{ type: 'key', key: 'C', value: help.showCalendar || 'Show calendar' },
|
|
82
|
+
{ type: 'key', key: 'I', value: help.showInsights || 'Show insights' },
|
|
75
83
|
{ type: 'key', key: '?', value: help.showHelp },
|
|
76
84
|
{ type: 'key', key: 'q', value: help.quit },
|
|
77
85
|
];
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { eq, and, gte } from 'drizzle-orm';
|
|
5
|
+
import { getDb, schema } from '../../db/index.js';
|
|
6
|
+
import { t, fmt } from '../../i18n/index.js';
|
|
7
|
+
import { useTheme } from '../theme/index.js';
|
|
8
|
+
const VISIBLE_LINES = 16;
|
|
9
|
+
function stringWidth(str) {
|
|
10
|
+
let width = 0;
|
|
11
|
+
for (const char of str) {
|
|
12
|
+
const code = char.codePointAt(0) || 0;
|
|
13
|
+
if ((code >= 0x1100 && code <= 0x115F) ||
|
|
14
|
+
(code >= 0x2E80 && code <= 0x303E) ||
|
|
15
|
+
(code >= 0x3040 && code <= 0x9FFF) ||
|
|
16
|
+
(code >= 0xAC00 && code <= 0xD7AF) ||
|
|
17
|
+
(code >= 0xF900 && code <= 0xFAFF) ||
|
|
18
|
+
(code >= 0xFE30 && code <= 0xFE6F) ||
|
|
19
|
+
(code >= 0xFF01 && code <= 0xFF60) ||
|
|
20
|
+
(code >= 0xFFE0 && code <= 0xFFE6) ||
|
|
21
|
+
(code >= 0x20000 && code <= 0x2FFFF)) {
|
|
22
|
+
width += 2;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
width += 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return width;
|
|
29
|
+
}
|
|
30
|
+
function padEndCJK(str, targetWidth) {
|
|
31
|
+
const currentWidth = stringWidth(str);
|
|
32
|
+
const padding = Math.max(0, targetWidth - currentWidth);
|
|
33
|
+
return str + ' '.repeat(padding);
|
|
34
|
+
}
|
|
35
|
+
function bar(count, max, width = 15) {
|
|
36
|
+
if (max === 0)
|
|
37
|
+
return '';
|
|
38
|
+
const filled = Math.round((count / max) * width);
|
|
39
|
+
return '█'.repeat(filled);
|
|
40
|
+
}
|
|
41
|
+
function getWeekStart(date) {
|
|
42
|
+
const d = new Date(date);
|
|
43
|
+
d.setDate(d.getDate() - d.getDay());
|
|
44
|
+
d.setHours(0, 0, 0, 0);
|
|
45
|
+
return d;
|
|
46
|
+
}
|
|
47
|
+
export function InsightsModal({ onClose }) {
|
|
48
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
49
|
+
const [content, setContent] = useState([]);
|
|
50
|
+
const [loading, setLoading] = useState(true);
|
|
51
|
+
const theme = useTheme();
|
|
52
|
+
const i18n = t();
|
|
53
|
+
const ins = i18n.commands.insights;
|
|
54
|
+
const isJa = ins?.title === 'タスクインサイト';
|
|
55
|
+
const l = useMemo(() => ({
|
|
56
|
+
title: ins?.title || 'Task Insights',
|
|
57
|
+
period: ins?.period || 'Period',
|
|
58
|
+
weeklyCompletion: ins?.weeklyCompletion || 'Weekly Completion',
|
|
59
|
+
weekLabel: ins?.weekLabel || 'Week of {date}',
|
|
60
|
+
tasksCompleted: ins?.tasksCompleted || '{count} tasks completed',
|
|
61
|
+
dailyBreakdown: ins?.dailyBreakdown || 'Daily Breakdown',
|
|
62
|
+
currentStatus: ins?.currentStatus || 'Current Status',
|
|
63
|
+
byContext: ins?.byContext || 'By Context',
|
|
64
|
+
byEffort: ins?.byEffort || 'By Effort',
|
|
65
|
+
noContext: ins?.noContext || 'No context',
|
|
66
|
+
noEffort: ins?.noEffort || 'No effort set',
|
|
67
|
+
projectProgress: ins?.projectProgress || 'Project Progress',
|
|
68
|
+
activeProjects: ins?.activeProjects || 'Active Projects',
|
|
69
|
+
averageCompletion: ins?.averageCompletion || 'Average Completion Time',
|
|
70
|
+
daysAverage: ins?.daysAverage || '{days} days',
|
|
71
|
+
noData: ins?.noData || 'No completed tasks in this period',
|
|
72
|
+
total: ins?.total || 'Total',
|
|
73
|
+
andMore: ins?.andMore || ' ... and {count} more',
|
|
74
|
+
}), [ins]);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const loadInsights = async () => {
|
|
77
|
+
const db = getDb();
|
|
78
|
+
const weeks = 2;
|
|
79
|
+
const lines = [];
|
|
80
|
+
const now = new Date();
|
|
81
|
+
const startDate = getWeekStart(now);
|
|
82
|
+
startDate.setDate(startDate.getDate() - (weeks - 1) * 7);
|
|
83
|
+
// Query completed tasks
|
|
84
|
+
const completedTasks = await db
|
|
85
|
+
.select()
|
|
86
|
+
.from(schema.tasks)
|
|
87
|
+
.where(and(eq(schema.tasks.status, 'done'), gte(schema.tasks.updatedAt, startDate), eq(schema.tasks.isProject, false)));
|
|
88
|
+
// Period header
|
|
89
|
+
lines.push({ type: 'text', value: `${l.period}: ${startDate.toLocaleDateString()} ~ ${now.toLocaleDateString()}` });
|
|
90
|
+
lines.push({ type: 'spacer', value: '' });
|
|
91
|
+
if (completedTasks.length === 0) {
|
|
92
|
+
lines.push({ type: 'text', value: l.noData });
|
|
93
|
+
setContent(lines);
|
|
94
|
+
setLoading(false);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Weekly completion
|
|
98
|
+
lines.push({ type: 'header', value: l.weeklyCompletion });
|
|
99
|
+
for (let i = 0; i < weeks; i++) {
|
|
100
|
+
const ws = getWeekStart(now);
|
|
101
|
+
ws.setDate(ws.getDate() - i * 7);
|
|
102
|
+
const we = new Date(ws);
|
|
103
|
+
we.setDate(we.getDate() + 7);
|
|
104
|
+
const weekTasks = completedTasks.filter(t => t.updatedAt >= ws && t.updatedAt < we);
|
|
105
|
+
const weekLabel = fmt(l.weekLabel, { date: ws.toLocaleDateString() });
|
|
106
|
+
const countLabel = fmt(l.tasksCompleted, { count: weekTasks.length });
|
|
107
|
+
lines.push({ type: 'text', value: `${weekLabel}: ${countLabel}` });
|
|
108
|
+
const maxShow = 5;
|
|
109
|
+
for (let j = 0; j < Math.min(weekTasks.length, maxShow); j++) {
|
|
110
|
+
const task = weekTasks[j];
|
|
111
|
+
lines.push({ type: 'text', value: ` [${task.id.slice(0, 8)}] ${task.title}` });
|
|
112
|
+
}
|
|
113
|
+
if (weekTasks.length > maxShow) {
|
|
114
|
+
lines.push({ type: 'text', value: fmt(l.andMore, { count: weekTasks.length - maxShow }) });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
lines.push({ type: 'spacer', value: '' });
|
|
118
|
+
// Daily breakdown
|
|
119
|
+
lines.push({ type: 'header', value: l.dailyBreakdown });
|
|
120
|
+
const dayCounts = new Array(7).fill(0);
|
|
121
|
+
for (const task of completedTasks) {
|
|
122
|
+
dayCounts[task.updatedAt.getDay()]++;
|
|
123
|
+
}
|
|
124
|
+
const maxDaily = Math.max(...dayCounts);
|
|
125
|
+
const dayNames = isJa
|
|
126
|
+
? ['日', '月', '火', '水', '木', '金', '土']
|
|
127
|
+
: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
128
|
+
for (let i = 0; i < 7; i++) {
|
|
129
|
+
const name = padEndCJK(dayNames[i], 4);
|
|
130
|
+
const count = String(dayCounts[i]).padStart(3);
|
|
131
|
+
lines.push({ type: 'bar', value: ` ${name}${count}`, barValue: dayCounts[i], barMax: maxDaily });
|
|
132
|
+
}
|
|
133
|
+
lines.push({ type: 'spacer', value: '' });
|
|
134
|
+
// Current status distribution
|
|
135
|
+
lines.push({ type: 'header', value: l.currentStatus });
|
|
136
|
+
const allTasks = await db
|
|
137
|
+
.select()
|
|
138
|
+
.from(schema.tasks)
|
|
139
|
+
.where(eq(schema.tasks.isProject, false));
|
|
140
|
+
const statusCounts = new Map();
|
|
141
|
+
for (const task of allTasks) {
|
|
142
|
+
statusCounts.set(task.status, (statusCounts.get(task.status) || 0) + 1);
|
|
143
|
+
}
|
|
144
|
+
const statusOrder = ['inbox', 'next', 'waiting', 'someday', 'done'];
|
|
145
|
+
const maxStatus = Math.max(...statusCounts.values());
|
|
146
|
+
for (const status of statusOrder) {
|
|
147
|
+
const count = statusCounts.get(status) || 0;
|
|
148
|
+
const label = padEndCJK(i18n.status[status] || status, 16);
|
|
149
|
+
const countStr = String(count).padStart(3);
|
|
150
|
+
lines.push({ type: 'bar', value: ` ${label}${countStr}`, barValue: count, barMax: maxStatus });
|
|
151
|
+
}
|
|
152
|
+
lines.push({ type: 'spacer', value: '' });
|
|
153
|
+
// Context distribution
|
|
154
|
+
lines.push({ type: 'header', value: l.byContext });
|
|
155
|
+
const contextMap = new Map();
|
|
156
|
+
for (const task of completedTasks) {
|
|
157
|
+
const key = task.context || l.noContext;
|
|
158
|
+
contextMap.set(key, (contextMap.get(key) || 0) + 1);
|
|
159
|
+
}
|
|
160
|
+
const contextEntries = Array.from(contextMap.entries()).sort((a, b) => b[1] - a[1]);
|
|
161
|
+
const maxContext = contextEntries.length > 0 ? contextEntries[0][1] : 0;
|
|
162
|
+
for (const [ctx, count] of contextEntries) {
|
|
163
|
+
const displayLabel = ctx === l.noContext ? ctx : `@${ctx}`;
|
|
164
|
+
const label = padEndCJK(displayLabel, 16);
|
|
165
|
+
const pct = Math.round((count / completedTasks.length) * 100);
|
|
166
|
+
const countStr = String(count).padStart(3);
|
|
167
|
+
lines.push({ type: 'bar', value: ` ${label}${countStr} (${String(pct).padStart(2)}%)`, barValue: count, barMax: maxContext });
|
|
168
|
+
}
|
|
169
|
+
lines.push({ type: 'spacer', value: '' });
|
|
170
|
+
// Effort distribution
|
|
171
|
+
lines.push({ type: 'header', value: l.byEffort });
|
|
172
|
+
const effortLabels = {
|
|
173
|
+
small: i18n.tui.effort?.small || 'Small',
|
|
174
|
+
medium: i18n.tui.effort?.medium || 'Medium',
|
|
175
|
+
large: i18n.tui.effort?.large || 'Large',
|
|
176
|
+
};
|
|
177
|
+
const effortMap = new Map();
|
|
178
|
+
for (const task of completedTasks) {
|
|
179
|
+
const key = task.effort ? (effortLabels[task.effort] || task.effort) : l.noEffort;
|
|
180
|
+
effortMap.set(key, (effortMap.get(key) || 0) + 1);
|
|
181
|
+
}
|
|
182
|
+
const effortEntries = Array.from(effortMap.entries()).sort((a, b) => b[1] - a[1]);
|
|
183
|
+
const maxEffort = effortEntries.length > 0 ? effortEntries[0][1] : 0;
|
|
184
|
+
for (const [eff, count] of effortEntries) {
|
|
185
|
+
const label = padEndCJK(eff, 16);
|
|
186
|
+
const pct = Math.round((count / completedTasks.length) * 100);
|
|
187
|
+
const countStr = String(count).padStart(3);
|
|
188
|
+
lines.push({ type: 'bar', value: ` ${label}${countStr} (${String(pct).padStart(2)}%)`, barValue: count, barMax: maxEffort });
|
|
189
|
+
}
|
|
190
|
+
lines.push({ type: 'spacer', value: '' });
|
|
191
|
+
// Project progress
|
|
192
|
+
lines.push({ type: 'header', value: l.projectProgress });
|
|
193
|
+
const activeProjects = await db
|
|
194
|
+
.select()
|
|
195
|
+
.from(schema.tasks)
|
|
196
|
+
.where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.status, 'next')));
|
|
197
|
+
lines.push({ type: 'text', value: ` ${l.activeProjects}: ${activeProjects.length}` });
|
|
198
|
+
for (const project of activeProjects) {
|
|
199
|
+
const children = await db
|
|
200
|
+
.select()
|
|
201
|
+
.from(schema.tasks)
|
|
202
|
+
.where(eq(schema.tasks.parentId, project.id));
|
|
203
|
+
const total = children.length;
|
|
204
|
+
const done = children.filter(c => c.status === 'done').length;
|
|
205
|
+
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
206
|
+
lines.push({ type: 'text', value: ` [${project.id.slice(0, 8)}] ${project.title} (${done}/${total}, ${pct}%)` });
|
|
207
|
+
}
|
|
208
|
+
lines.push({ type: 'spacer', value: '' });
|
|
209
|
+
// Average completion time
|
|
210
|
+
lines.push({ type: 'header', value: l.averageCompletion });
|
|
211
|
+
let totalMs = 0;
|
|
212
|
+
let validCount = 0;
|
|
213
|
+
for (const task of completedTasks) {
|
|
214
|
+
const diff = task.updatedAt.getTime() - task.createdAt.getTime();
|
|
215
|
+
if (diff > 0) {
|
|
216
|
+
totalMs += diff;
|
|
217
|
+
validCount++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (validCount > 0) {
|
|
221
|
+
const avgDays = totalMs / validCount / (1000 * 60 * 60 * 24);
|
|
222
|
+
if (avgDays < 1) {
|
|
223
|
+
const hours = Math.round(avgDays * 24);
|
|
224
|
+
const hoursLabel = isJa ? `${hours}時間` : `${hours}h`;
|
|
225
|
+
lines.push({ type: 'text', value: ` ${hoursLabel}` });
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
lines.push({ type: 'text', value: ` ${fmt(l.daysAverage, { days: avgDays.toFixed(1) })}` });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
lines.push({ type: 'spacer', value: '' });
|
|
232
|
+
// Total
|
|
233
|
+
lines.push({ type: 'text', value: `${l.total}: ${fmt(l.tasksCompleted, { count: completedTasks.length })}` });
|
|
234
|
+
setContent(lines);
|
|
235
|
+
setLoading(false);
|
|
236
|
+
};
|
|
237
|
+
loadInsights();
|
|
238
|
+
}, [l, isJa, i18n]);
|
|
239
|
+
const maxScroll = Math.max(0, content.length - VISIBLE_LINES);
|
|
240
|
+
useInput((input, key) => {
|
|
241
|
+
if (input === 'j' || key.downArrow) {
|
|
242
|
+
setScrollOffset(prev => Math.min(prev + 1, maxScroll));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (input === 'k' || key.upArrow) {
|
|
246
|
+
setScrollOffset(prev => Math.max(prev - 1, 0));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (key.escape || key.return || input === 'q' || input === ' ') {
|
|
250
|
+
onClose();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
const visibleContent = content.slice(scrollOffset, scrollOffset + VISIBLE_LINES);
|
|
255
|
+
const showScrollUp = scrollOffset > 0;
|
|
256
|
+
const showScrollDown = scrollOffset < maxScroll;
|
|
257
|
+
const formatTitle = (title) => theme.style.headerUppercase ? title.toUpperCase() : title;
|
|
258
|
+
const renderLine = (line, index) => {
|
|
259
|
+
switch (line.type) {
|
|
260
|
+
case 'header':
|
|
261
|
+
return (_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(line.value) }, index));
|
|
262
|
+
case 'bar':
|
|
263
|
+
return (_jsxs(Text, { color: theme.colors.text, children: [line.value, " ", _jsx(Text, { color: theme.colors.secondary, children: bar(line.barValue || 0, line.barMax || 0) })] }, index));
|
|
264
|
+
case 'spacer':
|
|
265
|
+
return _jsx(Text, { children: " " }, index);
|
|
266
|
+
default:
|
|
267
|
+
return (_jsx(Text, { color: theme.colors.text, children: line.value }, index));
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
const closeHint = isJa ? 'Esc/q: 閉じる' : 'Esc/q: close';
|
|
271
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.modal, borderColor: theme.colors.borderActive, paddingX: 2, paddingY: 1, children: [_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.secondary, children: formatTitle(l.title) }) }), _jsx(Box, { flexDirection: "column", height: VISIBLE_LINES + 2, children: loading ? (_jsx(Text, { color: theme.colors.textMuted, children: isJa ? '読み込み中...' : 'Loading...' })) : (_jsxs(_Fragment, { children: [showScrollUp && (_jsx(Text, { color: theme.colors.textMuted, children: " \u25B2 scroll up (k)" })), !showScrollUp && _jsx(Text, { children: " " }), visibleContent.map((line, index) => renderLine(line, index)), showScrollDown && (_jsx(Text, { color: theme.colors.textMuted, children: " \u25BC scroll down (j)" })), !showScrollDown && _jsx(Text, { children: " " })] })) }), _jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsxs(Text, { color: theme.colors.textMuted, children: [maxScroll > 0 ? 'j/k: scroll | ' : '', closeHint] }) })] }));
|
|
272
|
+
}
|
|
@@ -6,6 +6,7 @@ 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';
|
|
9
|
+
import { InsightsModal } from './InsightsModal.js';
|
|
9
10
|
import { CalendarModal } from './CalendarModal.js';
|
|
10
11
|
import { FunctionKeyBar } from './FunctionKeyBar.js';
|
|
11
12
|
import { SearchBar } from './SearchBar.js';
|
|
@@ -15,9 +16,9 @@ import { CalendarEvents } from './CalendarEvents.js';
|
|
|
15
16
|
import { getDb, schema } from '../../db/index.js';
|
|
16
17
|
import { t, fmt } from '../../i18n/index.js';
|
|
17
18
|
import { useTheme } from '../theme/index.js';
|
|
18
|
-
import { isTursoEnabled, getContexts, addContext, getContextFilter, setContextFilter as saveContextFilter } from '../../config.js';
|
|
19
|
+
import { isTursoEnabled, getContexts, addContext, getContextFilter, setContextFilter as saveContextFilter, getFocusFilter, setFocusFilter } from '../../config.js';
|
|
19
20
|
import { VERSION } from '../../version.js';
|
|
20
|
-
import { useHistory, CreateTaskCommand, MoveTaskCommand, LinkTaskCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from '../history/index.js';
|
|
21
|
+
import { useHistory, CreateTaskCommand, MoveTaskCommand, LinkTaskCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from '../history/index.js';
|
|
21
22
|
const COLUMNS = ['todo', 'doing', 'done'];
|
|
22
23
|
export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
23
24
|
const theme = useTheme();
|
|
@@ -54,7 +55,15 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
54
55
|
}, []);
|
|
55
56
|
const [contextSelectIndex, setContextSelectIndex] = useState(0);
|
|
56
57
|
const [availableContexts, setAvailableContexts] = useState([]);
|
|
58
|
+
const [focusFilter, setFocusFilterState] = useState(getFocusFilter());
|
|
59
|
+
const [effortSelectIndex, setEffortSelectIndex] = useState(0);
|
|
57
60
|
const i18n = t();
|
|
61
|
+
const EFFORT_OPTIONS = [
|
|
62
|
+
{ value: 'small', label: i18n.tui.effort?.small || 'Small' },
|
|
63
|
+
{ value: 'medium', label: i18n.tui.effort?.medium || 'Medium' },
|
|
64
|
+
{ value: 'large', label: i18n.tui.effort?.large || 'Large' },
|
|
65
|
+
{ value: null, label: i18n.tui.effort?.clear || 'Clear' },
|
|
66
|
+
];
|
|
58
67
|
// Status mapping:
|
|
59
68
|
// TODO = inbox + someday
|
|
60
69
|
// Doing = next + waiting
|
|
@@ -69,6 +78,12 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
69
78
|
return taskList.filter(t => !t.context);
|
|
70
79
|
return taskList.filter(t => t.context === contextFilter);
|
|
71
80
|
};
|
|
81
|
+
// Apply focus filter helper
|
|
82
|
+
const filterByFocus = (taskList) => {
|
|
83
|
+
if (!focusFilter)
|
|
84
|
+
return taskList;
|
|
85
|
+
return taskList.filter(t => t.isFocused);
|
|
86
|
+
};
|
|
72
87
|
// TODO: inbox + someday (non-project tasks)
|
|
73
88
|
let todoTasks = await db
|
|
74
89
|
.select()
|
|
@@ -92,13 +107,13 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
92
107
|
.from(schema.tasks)
|
|
93
108
|
.where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.status, 'next')));
|
|
94
109
|
setTasks({
|
|
95
|
-
todo: filterByContext(todoTasks),
|
|
96
|
-
doing: filterByContext(doingTasks),
|
|
97
|
-
done: filterByContext(doneTasks),
|
|
110
|
+
todo: filterByFocus(filterByContext(todoTasks)),
|
|
111
|
+
doing: filterByFocus(filterByContext(doingTasks)),
|
|
112
|
+
done: filterByFocus(filterByContext(doneTasks)),
|
|
98
113
|
});
|
|
99
114
|
setProjects(projectTasks);
|
|
100
115
|
setAvailableContexts(getContexts());
|
|
101
|
-
}, [contextFilter]);
|
|
116
|
+
}, [contextFilter, focusFilter]);
|
|
102
117
|
useEffect(() => {
|
|
103
118
|
loadTasks();
|
|
104
119
|
}, [loadTasks]);
|
|
@@ -150,6 +165,11 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
150
165
|
const currentColumn = COLUMNS[currentColumnIndex];
|
|
151
166
|
const currentTasks = tasks[currentColumn];
|
|
152
167
|
const selectedTaskIndex = selectedTaskIndices[currentColumn];
|
|
168
|
+
const getCurrentTask = () => {
|
|
169
|
+
if (currentTasks.length === 0)
|
|
170
|
+
return undefined;
|
|
171
|
+
return currentTasks[selectedTaskIndex];
|
|
172
|
+
};
|
|
153
173
|
// Get all tasks for search
|
|
154
174
|
const getAllTasks = useCallback(() => {
|
|
155
175
|
const allTasks = [];
|
|
@@ -346,13 +366,54 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
346
366
|
setMessage(description);
|
|
347
367
|
await loadTasks();
|
|
348
368
|
}, [i18n.tui.context, loadTasks, history]);
|
|
369
|
+
const toggleTaskFocus = async () => {
|
|
370
|
+
const task = getCurrentTask();
|
|
371
|
+
if (!task)
|
|
372
|
+
return;
|
|
373
|
+
const newFocused = !task.isFocused;
|
|
374
|
+
const description = newFocused
|
|
375
|
+
? fmt(i18n.tui.focus?.taskFocused || 'Focused: "{title}"', { title: task.title })
|
|
376
|
+
: fmt(i18n.tui.focus?.taskUnfocused || 'Unfocused: "{title}"', { title: task.title });
|
|
377
|
+
const command = new SetFocusCommand({
|
|
378
|
+
taskId: task.id,
|
|
379
|
+
fromFocused: task.isFocused,
|
|
380
|
+
toFocused: newFocused,
|
|
381
|
+
description,
|
|
382
|
+
});
|
|
383
|
+
await history.execute(command);
|
|
384
|
+
setMessage(description);
|
|
385
|
+
await loadTasks();
|
|
386
|
+
};
|
|
387
|
+
const toggleFocusFilter = () => {
|
|
388
|
+
const newValue = !focusFilter;
|
|
389
|
+
setFocusFilterState(newValue);
|
|
390
|
+
setFocusFilter(newValue);
|
|
391
|
+
setMessage(newValue ? (i18n.tui.focus?.filterOn || 'Focus filter ON') : (i18n.tui.focus?.filterOff || 'Focus filter OFF'));
|
|
392
|
+
};
|
|
393
|
+
const setTaskEffort = async (effort) => {
|
|
394
|
+
const task = getCurrentTask();
|
|
395
|
+
if (!task)
|
|
396
|
+
return;
|
|
397
|
+
const description = effort
|
|
398
|
+
? fmt(i18n.tui.effort?.effortSet || 'Set effort {effort} for "{title}"', { effort: i18n.tui.effort?.[effort] || effort, title: task.title })
|
|
399
|
+
: fmt(i18n.tui.effort?.effortCleared || 'Cleared effort for "{title}"', { title: task.title });
|
|
400
|
+
const command = new SetEffortCommand({
|
|
401
|
+
taskId: task.id,
|
|
402
|
+
fromEffort: task.effort,
|
|
403
|
+
toEffort: effort,
|
|
404
|
+
description,
|
|
405
|
+
});
|
|
406
|
+
await history.execute(command);
|
|
407
|
+
setMessage(description);
|
|
408
|
+
setMode('normal');
|
|
409
|
+
await loadTasks();
|
|
410
|
+
};
|
|
349
411
|
const getColumnLabel = (column) => {
|
|
350
412
|
return i18n.kanban[column];
|
|
351
413
|
};
|
|
352
414
|
useInput((input, key) => {
|
|
353
|
-
// Handle help mode -
|
|
354
|
-
if (mode === 'help') {
|
|
355
|
-
setMode('normal');
|
|
415
|
+
// Handle help/insights mode - let modals handle their own input
|
|
416
|
+
if (mode === 'help' || mode === 'insights') {
|
|
356
417
|
return;
|
|
357
418
|
}
|
|
358
419
|
// Handle search mode
|
|
@@ -533,6 +594,26 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
533
594
|
}
|
|
534
595
|
return;
|
|
535
596
|
}
|
|
597
|
+
// Handle set-effort mode
|
|
598
|
+
if (mode === 'set-effort') {
|
|
599
|
+
if (key.escape) {
|
|
600
|
+
setMode('normal');
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (input === 'j' || key.downArrow) {
|
|
604
|
+
setEffortSelectIndex(prev => Math.min(prev + 1, EFFORT_OPTIONS.length - 1));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (input === 'k' || key.upArrow) {
|
|
608
|
+
setEffortSelectIndex(prev => Math.max(prev - 1, 0));
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (key.return) {
|
|
612
|
+
setTaskEffort(EFFORT_OPTIONS[effortSelectIndex].value);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
536
617
|
// Clear message on any input
|
|
537
618
|
if (message) {
|
|
538
619
|
setMessage(null);
|
|
@@ -542,6 +623,11 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
542
623
|
setMode('help');
|
|
543
624
|
return;
|
|
544
625
|
}
|
|
626
|
+
// Show insights
|
|
627
|
+
if (input === 'I') {
|
|
628
|
+
setMode('insights');
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
545
631
|
// Show calendar
|
|
546
632
|
if (input === 'C') {
|
|
547
633
|
setMode('calendar');
|
|
@@ -567,6 +653,24 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
567
653
|
setMode('set-context');
|
|
568
654
|
return;
|
|
569
655
|
}
|
|
656
|
+
// Toggle task focus
|
|
657
|
+
if (input === 'g') {
|
|
658
|
+
toggleTaskFocus();
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
// Toggle focus filter
|
|
662
|
+
if (input === 'G') {
|
|
663
|
+
toggleFocusFilter();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
// Set effort
|
|
667
|
+
if (input === 'E') {
|
|
668
|
+
if (getCurrentTask()) {
|
|
669
|
+
setEffortSelectIndex(0);
|
|
670
|
+
setMode('set-effort');
|
|
671
|
+
}
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
570
674
|
// Settings: Theme selector
|
|
571
675
|
if (input === 'T' && onOpenSettings) {
|
|
572
676
|
onOpenSettings('theme-select');
|
|
@@ -719,6 +823,10 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
719
823
|
if (mode === 'help') {
|
|
720
824
|
return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(HelpModal, { onClose: () => setMode('normal'), isKanban: true }) }));
|
|
721
825
|
}
|
|
826
|
+
// Insights modal overlay
|
|
827
|
+
if (mode === 'insights') {
|
|
828
|
+
return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(InsightsModal, { onClose: () => setMode('normal') }) }));
|
|
829
|
+
}
|
|
722
830
|
// Calendar modal overlay
|
|
723
831
|
if (mode === 'calendar') {
|
|
724
832
|
return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(CalendarModal, { onClose: () => setMode('normal') }) }));
|
|
@@ -728,7 +836,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
728
836
|
const tursoEnabled = isTursoEnabled();
|
|
729
837
|
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'
|
|
730
838
|
? (tursoEnabled ? ' ☁️ turso' : ' 💾 local')
|
|
731
|
-
: (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? (i18n.tui.context?.none || 'none') : contextFilter] }))] }), _jsxs(Box, { children: [_jsx(CalendarEvents, { compact: true, showLabel: true, withSeparator: true }), _jsx(Clock, {}), _jsxs(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) => {
|
|
839
|
+
: (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? (i18n.tui.context?.none || 'none') : contextFilter] })), focusFilter && (_jsxs(Text, { color: theme.colors.accent, children: [" ", i18n.tui.focus?.focused || '★ Focused'] }))] }), _jsxs(Box, { children: [_jsx(CalendarEvents, { compact: true, showLabel: true, withSeparator: true }), _jsx(Clock, {}), _jsxs(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') })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.label || 'Effort', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.effort ? (i18n.tui.effort?.[selectedTask.effort] || selectedTask.effort) : '-' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.focus?.label || 'Focus', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.isFocused ? '★' : '-' })] }), 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) => {
|
|
732
840
|
const isSelected = index === selectedCommentIndex && mode === 'task-detail';
|
|
733
841
|
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));
|
|
734
842
|
})) }), 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) => {
|
|
@@ -751,7 +859,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
751
859
|
const isActive = (ctx === 'clear' && !currentContext) ||
|
|
752
860
|
(ctx !== 'clear' && ctx !== 'new' && currentContext === ctx);
|
|
753
861
|
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));
|
|
754
|
-
}) }), _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: [
|
|
862
|
+
}) }), _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 === 'set-effort' ? (_jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.modal, borderColor: theme.colors.borderActive, paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: i18n.tui.effort?.set || 'Set effort' }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.effort?.setHelp || 'j/k: select, Enter: confirm, Esc: cancel' }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: EFFORT_OPTIONS.map((option, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, option.label] }, option.label))) })] })) : (_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: [
|
|
755
863
|
{ key: 'i', label: i18n.tui.keyBar.comment },
|
|
756
864
|
{ key: 'd', label: i18n.tui.keyBar.delete },
|
|
757
865
|
{ key: 'P', label: i18n.tui.keyBar.project },
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { useTheme } from '../theme/index.js';
|
|
4
|
+
const EFFORT_LABELS = {
|
|
5
|
+
small: 'S',
|
|
6
|
+
medium: 'M',
|
|
7
|
+
large: 'L',
|
|
8
|
+
};
|
|
4
9
|
// Round border characters (DQ style)
|
|
5
10
|
const BORDER = {
|
|
6
11
|
topLeft: '╭',
|
|
@@ -51,13 +56,13 @@ export function KanbanColumn({ title, tasks, isActive, selectedTaskIndex, column
|
|
|
51
56
|
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, flexBasis: 0, marginRight: columnIndex < 2 ? 1 : 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: color, children: BORDER.topLeft }), _jsxs(Text, { color: color, children: [BORDER.horizontal.repeat(leftDashes), " "] }), _jsx(Text, { color: isActive ? theme.colors.accent : theme.colors.textMuted, bold: isActive, children: titleText }), _jsxs(Text, { color: color, children: [" ", BORDER.horizontal.repeat(rightDashes)] }), _jsx(Text, { color: color, children: BORDER.topRight }), showShadow && _jsx(Text, { children: " " })] }), _jsxs(Box, { flexDirection: "column", minHeight: 8, children: [tasks.length === 0 ? (_jsxs(Box, { children: [_jsx(Text, { color: color, children: BORDER.vertical }), _jsx(Box, { paddingX: 1, flexGrow: 1, children: _jsx(Text, { color: theme.colors.textMuted, italic: true, children: "-" }) }), _jsx(Text, { color: color, children: BORDER.vertical }), showShadow && _jsx(Text, { color: shadowColor, children: SHADOW })] })) : (tasks.map((task, index) => {
|
|
52
57
|
const isSelected = isActive && index === selectedTaskIndex;
|
|
53
58
|
const shortId = task.id.slice(0, 6);
|
|
54
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: color, children: BORDER.vertical }), _jsx(Box, { paddingX: 1, flexGrow: 1, children: _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix, "[", shortId, "] ", task.title] }) }), _jsx(Text, { color: color, children: BORDER.vertical }), showShadow && _jsx(Text, { color: shadowColor, children: SHADOW })] }, task.id));
|
|
59
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: color, children: BORDER.vertical }), _jsx(Box, { paddingX: 1, flexGrow: 1, children: _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix, task.isFocused ? '★ ' : '', task.effort ? `[${EFFORT_LABELS[task.effort]}] ` : '', "[", shortId, "] ", task.title] }) }), _jsx(Text, { color: color, children: BORDER.vertical }), showShadow && _jsx(Text, { color: shadowColor, children: SHADOW })] }, task.id));
|
|
55
60
|
})), Array.from({ length: Math.max(0, 8 - Math.max(1, tasks.length)) }).map((_, i) => (_jsxs(Box, { children: [_jsx(Text, { color: color, children: BORDER.vertical }), _jsx(Box, { paddingX: 1, flexGrow: 1, children: _jsx(Text, { children: " " }) }), _jsx(Text, { color: color, children: BORDER.vertical }), showShadow && _jsx(Text, { color: shadowColor, children: SHADOW })] }, `empty-${i}`)))] }), _jsxs(Box, { children: [_jsx(Text, { color: color, children: BORDER.bottomLeft }), _jsx(Text, { color: color, children: BORDER.horizontal.repeat(innerWidth) }), _jsx(Text, { color: color, children: BORDER.bottomRight }), showShadow && _jsx(Text, { color: shadowColor, children: SHADOW })] }), showShadow && (_jsx(Box, { children: _jsxs(Text, { color: shadowColor, children: [" ", SHADOW.repeat(innerWidth + 2)] }) }))] }));
|
|
56
61
|
}
|
|
57
62
|
// Default style
|
|
58
63
|
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, flexBasis: 0, borderStyle: theme.borders.list, borderColor: isActive ? theme.colors.borderActive : theme.colors.border, marginRight: columnIndex < 2 ? 1 : 0, children: [_jsx(Box, { paddingX: 1, justifyContent: "center", borderStyle: undefined, children: _jsxs(Text, { bold: true, color: isActive ? theme.colors.primary : theme.colors.textMuted, inverse: isActive && theme.style.tabActiveInverse, children: [title, " (", tasks.length, ")"] }) }), _jsx(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, minHeight: 8, children: tasks.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: "-" })) : (tasks.map((task, index) => {
|
|
59
64
|
const isSelected = isActive && index === selectedTaskIndex;
|
|
60
65
|
const shortId = task.id.slice(0, 6);
|
|
61
|
-
return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix, "[", shortId, "] ", task.title] }) }, task.id));
|
|
66
|
+
return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix, task.isFocused ? '★ ' : '', task.effort ? `[${EFFORT_LABELS[task.effort]}] ` : '', "[", shortId, "] ", task.title] }) }, task.id));
|
|
62
67
|
})) })] }));
|
|
63
68
|
}
|