floq 1.3.2 → 1.4.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 +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 +117 -9
- package/dist/ui/components/GtdMario.js +113 -8
- 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
|
@@ -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
|
}
|