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.
@@ -7,11 +7,12 @@ import { v4 as uuidv4 } from 'uuid';
7
7
  import { getDb, schema } from '../../db/index.js';
8
8
  import { t, fmt } from '../../i18n/index.js';
9
9
  import { useTheme } from '../theme/index.js';
10
- import { isTursoEnabled, getContexts, addContext, getLocale, getContextFilter, setContextFilter as saveContextFilter } from '../../config.js';
11
- import { useHistory, CreateTaskCommand, MoveTaskCommand, } from '../history/index.js';
10
+ import { isTursoEnabled, getContexts, addContext, getLocale, getContextFilter, setContextFilter as saveContextFilter, getFocusFilter, setFocusFilter } from '../../config.js';
11
+ import { useHistory, CreateTaskCommand, MoveTaskCommand, SetFocusCommand, SetEffortCommand, } from '../history/index.js';
12
12
  import { SearchBar } from './SearchBar.js';
13
13
  import { SearchResults } from './SearchResults.js';
14
14
  import { HelpModal } from './HelpModal.js';
15
+ import { InsightsModal } from './InsightsModal.js';
15
16
  import { CalendarModal } from './CalendarModal.js';
16
17
  import { Clock } from './Clock.js';
17
18
  import { CalendarEvents } from './CalendarEvents.js';
@@ -127,9 +128,23 @@ export function KanbanDQ({ onOpenSettings }) {
127
128
  const [searchQuery, setSearchQuery] = useState('');
128
129
  const [searchResults, setSearchResults] = useState([]);
129
130
  const [searchResultIndex, setSearchResultIndex] = useState(0);
131
+ // Focus filter state
132
+ const [focusFilter, setFocusFilterState] = useState(getFocusFilter());
133
+ const [effortSelectIndex, setEffortSelectIndex] = useState(0);
130
134
  const terminalWidth = stdout?.columns || 80;
131
135
  const leftPaneWidth = 20;
132
136
  const rightPaneWidth = terminalWidth - leftPaneWidth - 6;
137
+ const EFFORT_OPTIONS = [
138
+ { value: 'small', label: i18n.tui.effort?.small || 'Small' },
139
+ { value: 'medium', label: i18n.tui.effort?.medium || 'Medium' },
140
+ { value: 'large', label: i18n.tui.effort?.large || 'Large' },
141
+ { value: null, label: i18n.tui.effort?.clear || 'Clear' },
142
+ ];
143
+ const getCurrentTask = () => {
144
+ if (currentTasks.length === 0)
145
+ return undefined;
146
+ return currentTasks[selectedTaskIndex];
147
+ };
133
148
  const loadTasks = useCallback(async () => {
134
149
  const db = getDb();
135
150
  const filterByContext = (taskList) => {
@@ -139,6 +154,11 @@ export function KanbanDQ({ onOpenSettings }) {
139
154
  return taskList.filter(t => !t.context);
140
155
  return taskList.filter(t => t.context === contextFilter);
141
156
  };
157
+ const filterByFocus = (taskList) => {
158
+ if (!focusFilter)
159
+ return taskList;
160
+ return taskList.filter(t => t.isFocused);
161
+ };
142
162
  let todoTasks = await db
143
163
  .select()
144
164
  .from(schema.tasks)
@@ -154,12 +174,12 @@ export function KanbanDQ({ onOpenSettings }) {
154
174
  .from(schema.tasks)
155
175
  .where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false), gte(schema.tasks.updatedAt, oneWeekAgo)));
156
176
  setTasks({
157
- todo: filterByContext(todoTasks),
158
- doing: filterByContext(doingTasks),
159
- done: filterByContext(doneTasks),
177
+ todo: filterByFocus(filterByContext(todoTasks)),
178
+ doing: filterByFocus(filterByContext(doingTasks)),
179
+ done: filterByFocus(filterByContext(doneTasks)),
160
180
  });
161
181
  setAvailableContexts(getContexts());
162
- }, [contextFilter]);
182
+ }, [contextFilter, focusFilter]);
163
183
  const loadTaskComments = useCallback(async (taskId) => {
164
184
  const db = getDb();
165
185
  const comments = await db
@@ -275,6 +295,48 @@ export function KanbanDQ({ onOpenSettings }) {
275
295
  setMessage(fmt(i18n.tui.completed, { title: task.title }));
276
296
  await loadTasks();
277
297
  }, [i18n.tui.completed, loadTasks, history]);
298
+ const toggleTaskFocus = async () => {
299
+ const task = getCurrentTask();
300
+ if (!task)
301
+ return;
302
+ const newFocused = !task.isFocused;
303
+ const description = newFocused
304
+ ? fmt(i18n.tui.focus?.taskFocused || 'Focused: "{title}"', { title: task.title })
305
+ : fmt(i18n.tui.focus?.taskUnfocused || 'Unfocused: "{title}"', { title: task.title });
306
+ const command = new SetFocusCommand({
307
+ taskId: task.id,
308
+ fromFocused: task.isFocused,
309
+ toFocused: newFocused,
310
+ description,
311
+ });
312
+ await history.execute(command);
313
+ setMessage(description);
314
+ await loadTasks();
315
+ };
316
+ const toggleFocusFilter = () => {
317
+ const newValue = !focusFilter;
318
+ setFocusFilterState(newValue);
319
+ setFocusFilter(newValue);
320
+ setMessage(newValue ? (i18n.tui.focus?.filterOn || 'Focus filter ON') : (i18n.tui.focus?.filterOff || 'Focus filter OFF'));
321
+ };
322
+ const setTaskEffort = async (effort) => {
323
+ const task = getCurrentTask();
324
+ if (!task)
325
+ return;
326
+ const description = effort
327
+ ? fmt(i18n.tui.effort?.effortSet || 'Set effort {effort} for "{title}"', { effort: i18n.tui.effort?.[effort] || effort, title: task.title })
328
+ : fmt(i18n.tui.effort?.effortCleared || 'Cleared effort for "{title}"', { title: task.title });
329
+ const command = new SetEffortCommand({
330
+ taskId: task.id,
331
+ fromEffort: task.effort,
332
+ toEffort: effort,
333
+ description,
334
+ });
335
+ await history.execute(command);
336
+ setMessage(description);
337
+ setMode('normal');
338
+ await loadTasks();
339
+ };
278
340
  const handleInputSubmit = async (value) => {
279
341
  // Handle search mode submit
280
342
  if (mode === 'search') {
@@ -311,8 +373,8 @@ export function KanbanDQ({ onOpenSettings }) {
311
373
  }
312
374
  };
313
375
  useInput((input, key) => {
314
- // Handle help mode - let HelpModal handle its own input
315
- if (mode === 'help') {
376
+ // Handle help/insights mode - let modals handle their own input
377
+ if (mode === 'help' || mode === 'insights') {
316
378
  return;
317
379
  }
318
380
  if (mode === 'add' || mode === 'add-comment' || mode === 'add-context') {
@@ -375,6 +437,25 @@ export function KanbanDQ({ onOpenSettings }) {
375
437
  }
376
438
  return;
377
439
  }
440
+ if (mode === 'set-effort') {
441
+ if (key.escape) {
442
+ setMode('normal');
443
+ return;
444
+ }
445
+ if (input === 'j' || key.downArrow) {
446
+ setEffortSelectIndex(prev => Math.min(prev + 1, EFFORT_OPTIONS.length - 1));
447
+ return;
448
+ }
449
+ if (input === 'k' || key.upArrow) {
450
+ setEffortSelectIndex(prev => Math.max(prev - 1, 0));
451
+ return;
452
+ }
453
+ if (key.return) {
454
+ setTaskEffort(EFFORT_OPTIONS[effortSelectIndex].value);
455
+ return;
456
+ }
457
+ return;
458
+ }
378
459
  if (message)
379
460
  setMessage(null);
380
461
  // Quit
@@ -387,6 +468,11 @@ export function KanbanDQ({ onOpenSettings }) {
387
468
  setMode('help');
388
469
  return;
389
470
  }
471
+ // Insights
472
+ if (input === 'I') {
473
+ setMode('insights');
474
+ return;
475
+ }
390
476
  // Calendar
391
477
  if (input === 'C') {
392
478
  setMode('calendar');
@@ -488,6 +574,24 @@ export function KanbanDQ({ onOpenSettings }) {
488
574
  });
489
575
  return;
490
576
  }
577
+ // Toggle task focus
578
+ if (input === 'g') {
579
+ toggleTaskFocus();
580
+ return;
581
+ }
582
+ // Toggle focus filter
583
+ if (input === 'G') {
584
+ toggleFocusFilter();
585
+ return;
586
+ }
587
+ // Set effort
588
+ if (input === 'E') {
589
+ if (getCurrentTask()) {
590
+ setEffortSelectIndex(0);
591
+ setMode('set-effort');
592
+ }
593
+ return;
594
+ }
491
595
  }
492
596
  // Refresh
493
597
  if (input === 'r' && !key.ctrl) {
@@ -501,20 +605,25 @@ export function KanbanDQ({ onOpenSettings }) {
501
605
  if (mode === 'help') {
502
606
  return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(HelpModal, { onClose: () => setMode('normal'), isKanban: true }) }));
503
607
  }
608
+ // Insights modal overlay
609
+ if (mode === 'insights') {
610
+ return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(InsightsModal, { onClose: () => setMode('normal') }) }));
611
+ }
504
612
  // Calendar modal overlay
505
613
  if (mode === 'calendar') {
506
614
  return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(CalendarModal, { onClose: () => setMode('normal') }) }));
507
615
  }
508
- 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.accent, children: jobClass }), _jsx(Text, { color: theme.colors.text, children: " Lv." }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: Math.floor(tasks.done.length / 5) + 1 }), _jsx(Text, { color: theme.colors.text, children: " HP " }), _jsx(Text, { bold: true, color: theme.colors.statusNext, children: tasks.todo.length + tasks.doing.length }), _jsxs(Text, { color: theme.colors.textMuted, children: ["/", tasks.todo.length + tasks.doing.length + tasks.done.length] }), _jsx(Text, { color: theme.colors.text, children: " MP " }), _jsx(Text, { bold: true, color: theme.colors.statusWaiting, children: tasks.doing.length }), _jsx(Text, { color: theme.colors.accent, children: " [KANBAN]" }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: tursoEnabled ? ' [TURSO]' : '' }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? 'none' : contextFilter] }))] }), _jsxs(Box, { children: [_jsx(CalendarEvents, { compact: true, showLabel: true, withSeparator: true }), _jsx(Clock, {}), _jsx(Text, { color: theme.colors.textMuted, children: " ?=help q=quit" })] })] }), mode === 'context-filter' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "Filter by context" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
616
+ 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.accent, children: jobClass }), _jsx(Text, { color: theme.colors.text, children: " Lv." }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: Math.floor(tasks.done.length / 5) + 1 }), _jsx(Text, { color: theme.colors.text, children: " HP " }), _jsx(Text, { bold: true, color: theme.colors.statusNext, children: tasks.todo.length + tasks.doing.length }), _jsxs(Text, { color: theme.colors.textMuted, children: ["/", tasks.todo.length + tasks.doing.length + tasks.done.length] }), _jsx(Text, { color: theme.colors.text, children: " MP " }), _jsx(Text, { bold: true, color: theme.colors.statusWaiting, children: tasks.doing.length }), _jsx(Text, { color: theme.colors.accent, children: " [KANBAN]" }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: tursoEnabled ? ' [TURSO]' : '' }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? 'none' : contextFilter] })), focusFilter && (_jsx(Text, { color: theme.colors.accent, children: " [\u2605FOCUS]" }))] }), _jsxs(Box, { children: [_jsx(CalendarEvents, { compact: true, showLabel: true, withSeparator: true }), _jsx(Clock, {}), _jsx(Text, { color: theme.colors.textMuted, children: " ?=help q=quit" })] })] }), mode === 'context-filter' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "Filter by context" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
509
617
  const label = ctx === 'all' ? 'All' : ctx === 'none' ? 'No context' : `@${ctx}`;
510
618
  return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '▶ ' : ' ', label] }, ctx));
511
- }) })] })) : (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { marginRight: 2, children: _jsx(TitledBoxInline, { title: 'カテゴリ', width: leftPaneWidth, minHeight: 5, isActive: paneFocus === 'category', children: CATEGORIES.map((cat) => {
619
+ }) })] })) : mode === 'set-effort' ? (_jsx(TitledBoxInline, { title: i18n.tui.effort?.set || 'Set Effort', width: 30, isActive: true, children: EFFORT_OPTIONS.map((option, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '▶ ' : ' ', option.label] }, option.value || 'clear'))) })) : (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { marginRight: 2, children: _jsx(TitledBoxInline, { title: 'カテゴリ', width: leftPaneWidth, minHeight: 5, isActive: paneFocus === 'category', children: CATEGORIES.map((cat) => {
512
620
  const isSelected = cat === selectedCategory;
513
621
  const count = tasks[cat].length;
514
622
  return (_jsxs(Text, { color: isSelected && paneFocus === 'category' ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? '▶ ' : ' ', getCategoryLabel(cat), " (", count, ")"] }, cat));
515
623
  }) }) }), _jsx(Box, { flexGrow: 1, children: _jsx(TitledBoxInline, { title: getCategoryLabel(selectedCategory), width: rightPaneWidth, minHeight: 10, isActive: paneFocus === 'tasks', children: currentTasks.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noTasks })) : (currentTasks.map((task, index) => {
516
624
  const isSelected = paneFocus === 'tasks' && index === selectedTaskIndex;
517
- return (_jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? '' : ' ', task.title, task.context && _jsxs(Text, { color: theme.colors.muted, children: [" @", task.context] })] }, task.id));
625
+ const effortBadge = task.effort === 'small' ? '[S]' : task.effort === 'medium' ? '[M]' : task.effort === 'large' ? '[L]' : '';
626
+ return (_jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? '▶ ' : ' ', task.isFocused ? '★ ' : '', task.title, effortBadge && _jsxs(Text, { color: theme.colors.secondary, children: [" ", effortBadge] }), task.context && _jsxs(Text, { color: theme.colors.muted, children: [" @", task.context] })] }, task.id));
518
627
  })) }) })] })), 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 })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), mode === 'search' && searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery, viewMode: "kanban" })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textMuted, children: paneFocus === 'category'
519
628
  ? i18n.tui.kanbanFooter.category
520
629
  : i18n.tui.kanbanFooter.tasks }) })] }));
@@ -7,12 +7,13 @@ import { v4 as uuidv4 } from 'uuid';
7
7
  import { getDb, schema } from '../../db/index.js';
8
8
  import { t, fmt } from '../../i18n/index.js';
9
9
  import { useTheme } from '../theme/index.js';
10
- import { isTursoEnabled, getContexts, addContext, getContextFilter, setContextFilter as saveContextFilter } from '../../config.js';
10
+ import { isTursoEnabled, getContexts, addContext, getContextFilter, setContextFilter as saveContextFilter, getFocusFilter, setFocusFilter } from '../../config.js';
11
11
  import { VERSION } from '../../version.js';
12
- import { useHistory, CreateTaskCommand, MoveTaskCommand, } from '../history/index.js';
12
+ import { useHistory, CreateTaskCommand, MoveTaskCommand, SetFocusCommand, SetEffortCommand, } from '../history/index.js';
13
13
  import { SearchBar } from './SearchBar.js';
14
14
  import { SearchResults } from './SearchResults.js';
15
15
  import { HelpModal } from './HelpModal.js';
16
+ import { InsightsModal } from './InsightsModal.js';
16
17
  import { CalendarModal } from './CalendarModal.js';
17
18
  import { MarioBoxInline } from './MarioBox.js';
18
19
  import { Clock } from './Clock.js';
@@ -49,6 +50,19 @@ export function KanbanMario({ onOpenSettings }) {
49
50
  const [searchQuery, setSearchQuery] = useState('');
50
51
  const [searchResults, setSearchResults] = useState([]);
51
52
  const [searchResultIndex, setSearchResultIndex] = useState(0);
53
+ const [focusFilter, setFocusFilterState] = useState(getFocusFilter());
54
+ const [effortSelectIndex, setEffortSelectIndex] = useState(0);
55
+ const EFFORT_OPTIONS = [
56
+ { value: 'small', label: i18n.tui.effort?.small || 'Small' },
57
+ { value: 'medium', label: i18n.tui.effort?.medium || 'Medium' },
58
+ { value: 'large', label: i18n.tui.effort?.large || 'Large' },
59
+ { value: null, label: i18n.tui.effort?.clear || 'Clear' },
60
+ ];
61
+ const EFFORT_LABELS = {
62
+ small: 'S',
63
+ medium: 'M',
64
+ large: 'L',
65
+ };
52
66
  const terminalWidth = stdout?.columns || 80;
53
67
  const leftPaneWidth = 20;
54
68
  const rightPaneWidth = terminalWidth - leftPaneWidth - 6;
@@ -75,13 +89,18 @@ export function KanbanMario({ onOpenSettings }) {
75
89
  .select()
76
90
  .from(schema.tasks)
77
91
  .where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false), gte(schema.tasks.updatedAt, oneWeekAgo)));
92
+ const filterByFocus = (taskList) => {
93
+ if (!focusFilter)
94
+ return taskList;
95
+ return taskList.filter(t => t.isFocused);
96
+ };
78
97
  setTasks({
79
- todo: filterByContext(todoTasks),
80
- doing: filterByContext(doingTasks),
81
- done: filterByContext(doneTasks),
98
+ todo: filterByFocus(filterByContext(todoTasks)),
99
+ doing: filterByFocus(filterByContext(doingTasks)),
100
+ done: filterByFocus(filterByContext(doneTasks)),
82
101
  });
83
102
  setAvailableContexts(getContexts());
84
- }, [contextFilter]);
103
+ }, [contextFilter, focusFilter]);
85
104
  const loadTaskComments = useCallback(async (taskId) => {
86
105
  const db = getDb();
87
106
  const comments = await db
@@ -138,6 +157,53 @@ export function KanbanMario({ onOpenSettings }) {
138
157
  loadTasks();
139
158
  }, [loadTasks]);
140
159
  const currentTasks = tasks[selectedCategory];
160
+ const getCurrentTask = () => {
161
+ if (currentTasks.length === 0)
162
+ return undefined;
163
+ return currentTasks[selectedTaskIndex];
164
+ };
165
+ const toggleTaskFocus = async () => {
166
+ const task = getCurrentTask();
167
+ if (!task)
168
+ return;
169
+ const newFocused = !task.isFocused;
170
+ const description = newFocused
171
+ ? fmt(i18n.tui.focus?.taskFocused || 'Focused: "{title}"', { title: task.title })
172
+ : fmt(i18n.tui.focus?.taskUnfocused || 'Unfocused: "{title}"', { title: task.title });
173
+ const command = new SetFocusCommand({
174
+ taskId: task.id,
175
+ fromFocused: task.isFocused,
176
+ toFocused: newFocused,
177
+ description,
178
+ });
179
+ await history.execute(command);
180
+ setMessage(description);
181
+ await loadTasks();
182
+ };
183
+ const toggleFocusFilter = () => {
184
+ const newValue = !focusFilter;
185
+ setFocusFilterState(newValue);
186
+ setFocusFilter(newValue);
187
+ setMessage(newValue ? (i18n.tui.focus?.filterOn || 'Focus filter ON') : (i18n.tui.focus?.filterOff || 'Focus filter OFF'));
188
+ };
189
+ const setTaskEffort = async (effort) => {
190
+ const task = getCurrentTask();
191
+ if (!task)
192
+ return;
193
+ const description = effort
194
+ ? fmt(i18n.tui.effort?.effortSet || 'Set effort {effort} for "{title}"', { effort: i18n.tui.effort?.[effort] || effort, title: task.title })
195
+ : fmt(i18n.tui.effort?.effortCleared || 'Cleared effort for "{title}"', { title: task.title });
196
+ const command = new SetEffortCommand({
197
+ taskId: task.id,
198
+ fromEffort: task.effort,
199
+ toEffort: effort,
200
+ description,
201
+ });
202
+ await history.execute(command);
203
+ setMessage(description);
204
+ setMode('normal');
205
+ await loadTasks();
206
+ };
141
207
  const getCategoryLabel = (cat) => {
142
208
  return i18n.kanban[cat];
143
209
  };
@@ -233,8 +299,8 @@ export function KanbanMario({ onOpenSettings }) {
233
299
  }
234
300
  };
235
301
  useInput((input, key) => {
236
- // Handle help mode - let HelpModal handle its own input
237
- if (mode === 'help') {
302
+ // Handle help/insights mode - let modals handle their own input
303
+ if (mode === 'help' || mode === 'insights') {
238
304
  return;
239
305
  }
240
306
  if (mode === 'add' || mode === 'add-comment' || mode === 'add-context') {
@@ -297,6 +363,26 @@ export function KanbanMario({ onOpenSettings }) {
297
363
  }
298
364
  return;
299
365
  }
366
+ // Handle set-effort mode
367
+ if (mode === 'set-effort') {
368
+ if (key.escape) {
369
+ setMode('normal');
370
+ return;
371
+ }
372
+ if (input === 'j' || key.downArrow) {
373
+ setEffortSelectIndex(prev => Math.min(prev + 1, EFFORT_OPTIONS.length - 1));
374
+ return;
375
+ }
376
+ if (input === 'k' || key.upArrow) {
377
+ setEffortSelectIndex(prev => Math.max(prev - 1, 0));
378
+ return;
379
+ }
380
+ if (key.return) {
381
+ setTaskEffort(EFFORT_OPTIONS[effortSelectIndex].value);
382
+ return;
383
+ }
384
+ return;
385
+ }
300
386
  if (message)
301
387
  setMessage(null);
302
388
  // Quit
@@ -309,6 +395,11 @@ export function KanbanMario({ onOpenSettings }) {
309
395
  setMode('help');
310
396
  return;
311
397
  }
398
+ // Insights
399
+ if (input === 'I') {
400
+ setMode('insights');
401
+ return;
402
+ }
312
403
  // Calendar
313
404
  if (input === 'C') {
314
405
  setMode('calendar');
@@ -397,6 +488,24 @@ export function KanbanMario({ onOpenSettings }) {
397
488
  });
398
489
  return;
399
490
  }
491
+ // Toggle focus
492
+ if (input === 'g') {
493
+ toggleTaskFocus();
494
+ return;
495
+ }
496
+ // Toggle focus filter
497
+ if (input === 'G') {
498
+ toggleFocusFilter();
499
+ return;
500
+ }
501
+ // Set effort
502
+ if (input === 'E') {
503
+ if (getCurrentTask()) {
504
+ setEffortSelectIndex(0);
505
+ setMode('set-effort');
506
+ }
507
+ return;
508
+ }
400
509
  // Undo
401
510
  if (input === 'u') {
402
511
  history.undo().then((didUndo) => {
@@ -423,11 +532,15 @@ export function KanbanMario({ onOpenSettings }) {
423
532
  if (mode === 'help') {
424
533
  return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(HelpModal, { onClose: () => setMode('normal'), isKanban: true }) }));
425
534
  }
535
+ // Insights modal overlay
536
+ if (mode === 'insights') {
537
+ return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(InsightsModal, { onClose: () => setMode('normal') }) }));
538
+ }
426
539
  // Calendar modal overlay
427
540
  if (mode === 'calendar') {
428
541
  return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(CalendarModal, { onClose: () => setMode('normal') }) }));
429
542
  }
430
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "WORLD " }), _jsxs(Text, { color: theme.colors.primary, bold: true, children: [(new Date().getMonth() + 1), "-", new Date().getDate()] }), _jsx(Text, { color: theme.colors.text, children: " " }), _jsx(Text, { color: theme.colors.secondary, children: "x" }), _jsx(Text, { color: theme.colors.text, children: (tasks.todo.length + tasks.doing.length).toString().padStart(2, '0') }), _jsxs(Text, { color: theme.colors.textMuted, children: [" FLOQ v", VERSION] }), _jsx(Text, { color: theme.colors.accent, children: " [KANBAN]" }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: tursoEnabled ? ' [TURSO]' : ' [LOCAL]' }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? 'none' : contextFilter] }))] }), _jsxs(Box, { children: [_jsx(CalendarEvents, { compact: true, showLabel: true, withSeparator: true }), _jsx(Clock, {})] })] }), mode === 'context-filter' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "Filter by context" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
543
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "WORLD " }), _jsxs(Text, { color: theme.colors.primary, bold: true, children: [(new Date().getMonth() + 1), "-", new Date().getDate()] }), _jsx(Text, { color: theme.colors.text, children: " " }), _jsx(Text, { color: theme.colors.secondary, children: "x" }), _jsx(Text, { color: theme.colors.text, children: (tasks.todo.length + tasks.doing.length).toString().padStart(2, '0') }), _jsxs(Text, { color: theme.colors.textMuted, children: [" FLOQ v", VERSION] }), _jsx(Text, { color: theme.colors.accent, children: " [KANBAN]" }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: tursoEnabled ? ' [TURSO]' : ' [LOCAL]' }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? '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, {})] })] }), mode === 'context-filter' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "Filter by context" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
431
544
  const label = ctx === 'all' ? 'All' : ctx === 'none' ? 'No context' : `@${ctx}`;
432
545
  return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '🍄 ' : ' ', label] }, ctx));
433
546
  }) })] })) : (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { marginRight: 2, children: _jsx(MarioBoxInline, { title: 'STAGE', width: leftPaneWidth, minHeight: 5, isActive: paneFocus === 'category', children: CATEGORIES.map((cat) => {
@@ -436,8 +549,9 @@ export function KanbanMario({ onOpenSettings }) {
436
549
  return (_jsxs(Text, { color: isSelected && paneFocus === 'category' ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? '🍄 ' : ' ', getCategoryLabel(cat), " (", count, ")"] }, cat));
437
550
  }) }) }), _jsx(Box, { flexGrow: 1, children: _jsx(MarioBoxInline, { title: getCategoryLabel(selectedCategory), width: rightPaneWidth, minHeight: 10, isActive: paneFocus === 'tasks', children: currentTasks.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noTasks })) : (currentTasks.map((task, index) => {
438
551
  const isSelected = paneFocus === 'tasks' && index === selectedTaskIndex;
439
- return (_jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? '🍄 ' : ' ', task.title, task.context && _jsxs(Text, { color: theme.colors.muted, children: [" @", task.context] })] }, task.id));
440
- })) }) })] })), 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 })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), mode === 'search' && searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery, viewMode: "kanban" })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textMuted, children: paneFocus === 'category'
552
+ const shortId = task.id.slice(0, 4);
553
+ return (_jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? '🍄 ' : ' ', task.isFocused ? '★ ' : '', task.effort ? `[${EFFORT_LABELS[task.effort]}] ` : '', "[", shortId, "] ", task.title, task.context && _jsxs(Text, { color: theme.colors.muted, children: [" @", task.context] })] }, task.id));
554
+ })) }) })] })), 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 })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), mode === 'search' && searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery, viewMode: "kanban" })), mode === 'set-effort' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(MarioBoxInline, { title: i18n.tui.effort?.set || 'Set effort', width: 30, minHeight: EFFORT_OPTIONS.length + 2, isActive: true, children: EFFORT_OPTIONS.map((option, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '🍄 ' : ' ', option.label] }, option.label))) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.effort?.setHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textMuted, children: paneFocus === 'category'
441
555
  ? i18n.tui.kanbanFooter.category
442
556
  : i18n.tui.kanbanFooter.tasks }) })] }));
443
557
  }
@@ -1,11 +1,16 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { t } from '../../i18n/index.js';
4
4
  import { useTheme } from '../theme/index.js';
5
5
  import { ProgressBar } from './ProgressBar.js';
6
+ const EFFORT_LABELS = {
7
+ small: 'S',
8
+ medium: 'M',
9
+ large: 'L',
10
+ };
6
11
  export function TaskItem({ task, isSelected, projectName, progress }) {
7
12
  const shortId = task.id.slice(0, 8);
8
13
  const i18n = t();
9
14
  const theme = useTheme();
10
- 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.context && (_jsxs(Text, { color: theme.colors.accent, children: [" @", task.context] })), projectName && (_jsxs(Text, { color: theme.colors.statusSomeday, children: [" [", projectName, "]"] })), progress && progress.total > 0 && (_jsx(ProgressBar, { completed: progress.completed, total: progress.total })), task.waitingFor && (_jsxs(Text, { color: theme.colors.statusWaiting, children: [" (", i18n.status.waiting.toLowerCase(), ": ", task.waitingFor, ")"] })), task.dueDate && (_jsxs(Text, { color: theme.colors.accent, children: [" (due: ", task.dueDate.toLocaleDateString(), ")"] }))] }) }));
15
+ 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 && _jsx(Text, { color: theme.colors.accent, children: "\u2605 " }), "[", shortId, "] ", task.effort && _jsxs(Text, { color: theme.colors.secondary, children: ["[", EFFORT_LABELS[task.effort], "] "] }), task.title, task.context && (_jsxs(Text, { color: theme.colors.accent, children: [" @", task.context] })), projectName && (_jsxs(Text, { color: theme.colors.statusSomeday, children: [" [", projectName, "]"] })), progress && progress.total > 0 && (_jsx(ProgressBar, { completed: progress.completed, total: progress.total })), task.waitingFor && (_jsxs(Text, { color: theme.colors.statusWaiting, children: [" (", i18n.status.waiting.toLowerCase(), ": ", task.waitingFor, ")"] })), task.dueDate && (_jsxs(Text, { color: theme.colors.accent, children: [" (due: ", task.dueDate.toLocaleDateString(), ")"] }))] }) }));
11
16
  }
@@ -0,0 +1,20 @@
1
+ import type { UndoableCommand } from '../types.js';
2
+ interface SetEffortParams {
3
+ taskId: string;
4
+ fromEffort: string | null;
5
+ toEffort: string | null;
6
+ description: string;
7
+ }
8
+ /**
9
+ * Command to set/change a task's effort size
10
+ */
11
+ export declare class SetEffortCommand implements UndoableCommand {
12
+ readonly description: string;
13
+ private readonly taskId;
14
+ private readonly fromEffort;
15
+ private readonly toEffort;
16
+ constructor(params: SetEffortParams);
17
+ execute(): Promise<void>;
18
+ undo(): Promise<void>;
19
+ }
20
+ export {};
@@ -0,0 +1,37 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { getDb, schema } from '../../../db/index.js';
3
+ /**
4
+ * Command to set/change a task's effort size
5
+ */
6
+ export class SetEffortCommand {
7
+ description;
8
+ taskId;
9
+ fromEffort;
10
+ toEffort;
11
+ constructor(params) {
12
+ this.taskId = params.taskId;
13
+ this.fromEffort = params.fromEffort;
14
+ this.toEffort = params.toEffort;
15
+ this.description = params.description;
16
+ }
17
+ async execute() {
18
+ const db = getDb();
19
+ await db
20
+ .update(schema.tasks)
21
+ .set({
22
+ effort: this.toEffort,
23
+ updatedAt: new Date(),
24
+ })
25
+ .where(eq(schema.tasks.id, this.taskId));
26
+ }
27
+ async undo() {
28
+ const db = getDb();
29
+ await db
30
+ .update(schema.tasks)
31
+ .set({
32
+ effort: this.fromEffort,
33
+ updatedAt: new Date(),
34
+ })
35
+ .where(eq(schema.tasks.id, this.taskId));
36
+ }
37
+ }
@@ -0,0 +1,20 @@
1
+ import type { UndoableCommand } from '../types.js';
2
+ interface SetFocusParams {
3
+ taskId: string;
4
+ fromFocused: boolean;
5
+ toFocused: boolean;
6
+ description: string;
7
+ }
8
+ /**
9
+ * Command to toggle a task's focus state
10
+ */
11
+ export declare class SetFocusCommand implements UndoableCommand {
12
+ readonly description: string;
13
+ private readonly taskId;
14
+ private readonly fromFocused;
15
+ private readonly toFocused;
16
+ constructor(params: SetFocusParams);
17
+ execute(): Promise<void>;
18
+ undo(): Promise<void>;
19
+ }
20
+ export {};
@@ -0,0 +1,37 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { getDb, schema } from '../../../db/index.js';
3
+ /**
4
+ * Command to toggle a task's focus state
5
+ */
6
+ export class SetFocusCommand {
7
+ description;
8
+ taskId;
9
+ fromFocused;
10
+ toFocused;
11
+ constructor(params) {
12
+ this.taskId = params.taskId;
13
+ this.fromFocused = params.fromFocused;
14
+ this.toFocused = params.toFocused;
15
+ this.description = params.description;
16
+ }
17
+ async execute() {
18
+ const db = getDb();
19
+ await db
20
+ .update(schema.tasks)
21
+ .set({
22
+ isFocused: this.toFocused,
23
+ updatedAt: new Date(),
24
+ })
25
+ .where(eq(schema.tasks.id, this.taskId));
26
+ }
27
+ async undo() {
28
+ const db = getDb();
29
+ await db
30
+ .update(schema.tasks)
31
+ .set({
32
+ isFocused: this.fromFocused,
33
+ updatedAt: new Date(),
34
+ })
35
+ .where(eq(schema.tasks.id, this.taskId));
36
+ }
37
+ }
@@ -6,3 +6,5 @@ export { ConvertToProjectCommand } from './ConvertToProjectCommand.js';
6
6
  export { CreateCommentCommand } from './CreateCommentCommand.js';
7
7
  export { DeleteCommentCommand } from './DeleteCommentCommand.js';
8
8
  export { SetContextCommand } from './SetContextCommand.js';
9
+ export { SetFocusCommand } from './SetFocusCommand.js';
10
+ export { SetEffortCommand } from './SetEffortCommand.js';
@@ -6,3 +6,5 @@ export { ConvertToProjectCommand } from './ConvertToProjectCommand.js';
6
6
  export { CreateCommentCommand } from './CreateCommentCommand.js';
7
7
  export { DeleteCommentCommand } from './DeleteCommentCommand.js';
8
8
  export { SetContextCommand } from './SetContextCommand.js';
9
+ export { SetFocusCommand } from './SetFocusCommand.js';
10
+ export { SetEffortCommand } from './SetEffortCommand.js';
@@ -3,4 +3,4 @@ export { MAX_HISTORY_SIZE } from './types.js';
3
3
  export { HistoryManager, getHistoryManager } from './HistoryManager.js';
4
4
  export { HistoryProvider, useHistoryContext } from './HistoryContext.js';
5
5
  export { useHistory } from './useHistory.js';
6
- export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from './commands/index.js';
6
+ export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from './commands/index.js';
@@ -5,4 +5,4 @@ export { HistoryManager, getHistoryManager } from './HistoryManager.js';
5
5
  export { HistoryProvider, useHistoryContext } from './HistoryContext.js';
6
6
  export { useHistory } from './useHistory.js';
7
7
  // Commands
8
- export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from './commands/index.js';
8
+ export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from './commands/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "floq",
3
- "version": "1.3.3",
3
+ "version": "1.4.1",
4
4
  "description": "Floq - Getting Things Done Task Manager with MS-DOS style themes",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",