floq 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja.md CHANGED
@@ -7,10 +7,10 @@ MS-DOSスタイルのテーマを備えたターミナルベースのGTD(Getti
7
7
  ## 特徴
8
8
 
9
9
  - **TUIインターフェース**: Ink(CLI用React)で構築されたインタラクティブなターミナルUI
10
- - **GTDワークフロー**: Inbox、Next Actions、Waiting For、Someday/Maybe、Done
10
+ - **GTDワークフロー**: Inbox、Next Actions、Waiting For、Someday/Maybe、Done(過去7日間表示)
11
11
  - **カンバンモード**: 3カラムのカンバンボード表示(TODO、Doing、Done)
12
12
  - **プロジェクト**: タスクをプロジェクトに整理(進捗バー表示付き)
13
- - **コンテキスト**: タスクにコンテキスト(@work、@homeなど)を設定してフィルタリング
13
+ - **コンテキスト**: タスクにコンテキスト(@work、@homeなど)を設定してフィルタリング。タスク追加時は現在のフィルターを自動継承
14
14
  - **タスク検索**: `/` キーで全タスクを素早く検索
15
15
  - **コメント**: タスクにメモやコメントを追加
16
16
  - **クラウド同期**: [Turso](https://turso.tech/)のembedded replicasによるオプションの同期機能
package/README.md CHANGED
@@ -7,10 +7,10 @@ A terminal-based GTD (Getting Things Done) task manager with MS-DOS style themes
7
7
  ## Features
8
8
 
9
9
  - **TUI Interface**: Interactive terminal UI built with Ink (React for CLI)
10
- - **GTD Workflow**: Inbox, Next Actions, Waiting For, Someday/Maybe, Done
10
+ - **GTD Workflow**: Inbox, Next Actions, Waiting For, Someday/Maybe, Done (shows last 7 days)
11
11
  - **Kanban Mode**: 3-column kanban board view (TODO, Doing, Done)
12
12
  - **Projects**: Organize tasks into projects with progress tracking
13
- - **Contexts**: Tag tasks with contexts (@work, @home, etc.) and filter by context
13
+ - **Contexts**: Tag tasks with contexts (@work, @home, etc.) and filter by context. New tasks inherit the active context filter
14
14
  - **Task Search**: Quick search across all tasks with `/`
15
15
  - **Comments**: Add notes and comments to tasks
16
16
  - **Cloud Sync**: Optional sync with [Turso](https://turso.tech/) using embedded replicas
@@ -1,4 +1,4 @@
1
- import { eq } from 'drizzle-orm';
1
+ import { eq, and, gte } from 'drizzle-orm';
2
2
  import { getDb, schema } from '../db/index.js';
3
3
  import { t, fmt } from '../i18n/index.js';
4
4
  export async function listTasks(status) {
@@ -11,10 +11,22 @@ export async function listTasks(status) {
11
11
  console.error(fmt(i18n.commands.list.validStatuses, { statuses: validStatuses.join(', ') }));
12
12
  process.exit(1);
13
13
  }
14
- const tasks = await db
15
- .select()
16
- .from(schema.tasks)
17
- .where(eq(schema.tasks.status, status));
14
+ // For done status, only show tasks from the last week by default
15
+ let tasks;
16
+ if (status === 'done') {
17
+ const oneWeekAgo = new Date();
18
+ oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
19
+ tasks = await db
20
+ .select()
21
+ .from(schema.tasks)
22
+ .where(and(eq(schema.tasks.status, 'done'), gte(schema.tasks.updatedAt, oneWeekAgo)));
23
+ }
24
+ else {
25
+ tasks = await db
26
+ .select()
27
+ .from(schema.tasks)
28
+ .where(eq(schema.tasks.status, status));
29
+ }
18
30
  console.log(`\n${i18n.status[status]} (${tasks.length})`);
19
31
  console.log('─'.repeat(40));
20
32
  if (tasks.length === 0) {
package/dist/ui/App.js CHANGED
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useState, useEffect, useCallback } from 'react';
3
3
  import { Box, Text, useInput, useApp } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
- import { eq, and } from 'drizzle-orm';
5
+ import { eq, and, gte } from 'drizzle-orm';
6
6
  import { v4 as uuidv4 } from 'uuid';
7
7
  import { TaskItem } from './components/TaskItem.js';
8
8
  import { HelpModal } from './components/HelpModal.js';
@@ -105,10 +105,21 @@ function AppContent({ onOpenSettings }) {
105
105
  // Load all tasks (including project children) by status
106
106
  const statusList = ['inbox', 'next', 'waiting', 'someday', 'done'];
107
107
  for (const status of statusList) {
108
+ // For done tasks, only show those completed in the last week
109
+ const oneWeekAgo = new Date();
110
+ oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
111
+ const conditions = [
112
+ eq(schema.tasks.status, status),
113
+ eq(schema.tasks.isProject, false),
114
+ ];
115
+ // Filter done tasks to last week only
116
+ if (status === 'done') {
117
+ conditions.push(gte(schema.tasks.updatedAt, oneWeekAgo));
118
+ }
108
119
  let allTasks = await db
109
120
  .select()
110
121
  .from(schema.tasks)
111
- .where(and(eq(schema.tasks.status, status), eq(schema.tasks.isProject, false)));
122
+ .where(and(...conditions));
112
123
  // Apply context filter
113
124
  if (contextFilter !== null) {
114
125
  if (contextFilter === '') {
@@ -205,7 +216,7 @@ function AppContent({ onOpenSettings }) {
205
216
  setMode('normal');
206
217
  }
207
218
  }, [tasks]);
208
- const addTask = useCallback(async (title, parentId) => {
219
+ const addTask = useCallback(async (title, parentId, context) => {
209
220
  if (!title.trim())
210
221
  return;
211
222
  const now = new Date();
@@ -216,6 +227,7 @@ function AppContent({ onOpenSettings }) {
216
227
  title: title.trim(),
217
228
  status: parentId ? 'next' : 'inbox',
218
229
  parentId: parentId || null,
230
+ context: context || null,
219
231
  createdAt: now,
220
232
  updatedAt: now,
221
233
  },
@@ -298,12 +310,13 @@ function AppContent({ onOpenSettings }) {
298
310
  setMode('task-detail');
299
311
  }
300
312
  else if (mode === 'add-to-project' && selectedProject) {
301
- await addTask(value, selectedProject.id);
313
+ await addTask(value, selectedProject.id, contextFilter && contextFilter !== '' ? contextFilter : null);
302
314
  await loadProjectTasks(selectedProject.id);
303
315
  setMode('project-detail');
304
316
  }
305
317
  else {
306
- await addTask(value);
318
+ // Pass contextFilter when adding a task, so it inherits the current filter context
319
+ await addTask(value, undefined, contextFilter && contextFilter !== '' ? contextFilter : null);
307
320
  setMode('normal');
308
321
  }
309
322
  }
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useState, useEffect, useCallback } from 'react';
3
3
  import { Box, Text, useInput, useApp } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
- import { eq, and, inArray } from 'drizzle-orm';
5
+ import { eq, and, inArray, gte } from 'drizzle-orm';
6
6
  import { v4 as uuidv4 } from 'uuid';
7
7
  import { KanbanColumn } from './KanbanColumn.js';
8
8
  import { HelpModal } from './HelpModal.js';
@@ -72,11 +72,13 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
72
72
  .select()
73
73
  .from(schema.tasks)
74
74
  .where(and(inArray(schema.tasks.status, ['next', 'waiting']), eq(schema.tasks.isProject, false)));
75
- // Done: done (non-project tasks)
75
+ // Done: done (non-project tasks) - only show last week
76
+ const oneWeekAgo = new Date();
77
+ oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
76
78
  let doneTasks = await db
77
79
  .select()
78
80
  .from(schema.tasks)
79
- .where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false)));
81
+ .where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false), gte(schema.tasks.updatedAt, oneWeekAgo)));
80
82
  // Load projects for linking
81
83
  const projectTasks = await db
82
84
  .select()
@@ -189,7 +191,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
189
191
  setMode('normal');
190
192
  }
191
193
  }, [tasks]);
192
- const addTask = useCallback(async (title) => {
194
+ const addTask = useCallback(async (title, context) => {
193
195
  if (!title.trim())
194
196
  return;
195
197
  const now = new Date();
@@ -199,6 +201,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
199
201
  id: taskId,
200
202
  title: title.trim(),
201
203
  status: 'inbox', // New tasks go to inbox (which maps to TODO)
204
+ context: context || null,
202
205
  createdAt: now,
203
206
  updatedAt: now,
204
207
  },
@@ -249,7 +252,8 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
249
252
  return;
250
253
  }
251
254
  if (value.trim()) {
252
- await addTask(value);
255
+ // Pass contextFilter when adding a task, so it inherits the current filter context
256
+ await addTask(value, contextFilter && contextFilter !== '' ? contextFilter : null);
253
257
  }
254
258
  setInputValue('');
255
259
  setMode('normal');
@@ -711,7 +715,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
711
715
  : (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? (i18n.tui.context?.none || 'none') : contextFilter] }))] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), (mode === 'task-detail' || mode === 'add-comment' || mode === 'select-project') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📋 ' : '>> ', i18n.tui.taskDetailTitle || 'Task Details'] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ", ", i18n.tui.commentHint || 'i: add comment', ")"] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus, ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), selectedTask.dueDate && (_jsxs(Text, { color: theme.colors.textMuted, children: ["Due: ", selectedTask.dueDate.toLocaleDateString()] }))] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.comments || 'Comments', " (", taskComments.length, ")"] }) }), _jsx(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, minHeight: 5, children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
712
716
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
713
717
  return (_jsxs(Box, { flexDirection: "row", marginBottom: 1, children: [_jsx(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.textMuted, children: isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.textMuted, children: ["[", comment.createdAt.toLocaleString(), "]"] }), _jsx(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: comment.content })] })] }, comment.id));
714
- })) }), 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 })) })) : 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) => {
718
+ })) }), mode === 'add-comment' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.addComment || 'New comment: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'select-project' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", selectedTask.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, project.title] }, project.id))) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] }))] })) : mode === 'search' ? (_jsx(_Fragment, { children: searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery, viewMode: "kanban" })) })) : mode === 'context-filter' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.filter || 'Filter by context' }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
715
719
  const label = ctx === 'all'
716
720
  ? (i18n.tui.context?.all || 'All')
717
721
  : ctx === 'none'
@@ -4,6 +4,7 @@ interface SearchResultsProps {
4
4
  results: Task[];
5
5
  selectedIndex: number;
6
6
  query: string;
7
+ viewMode?: 'gtd' | 'kanban';
7
8
  }
8
- export declare function SearchResults({ results, selectedIndex, query }: SearchResultsProps): React.ReactElement;
9
+ export declare function SearchResults({ results, selectedIndex, query, viewMode }: SearchResultsProps): React.ReactElement;
9
10
  export {};
@@ -2,7 +2,17 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { Box, Text } from 'ink';
3
3
  import { t } from '../../i18n/index.js';
4
4
  import { useTheme } from '../theme/index.js';
5
- export function SearchResults({ results, selectedIndex, query }) {
5
+ // Map GTD status to Kanban column
6
+ function getKanbanColumn(status) {
7
+ if (status === 'inbox' || status === 'someday') {
8
+ return 'todo';
9
+ }
10
+ if (status === 'next' || status === 'waiting') {
11
+ return 'doing';
12
+ }
13
+ return 'done';
14
+ }
15
+ export function SearchResults({ results, selectedIndex, query, viewMode = 'gtd' }) {
6
16
  const i18n = t();
7
17
  const theme = useTheme();
8
18
  const search = i18n.tui.search;
@@ -12,7 +22,17 @@ export function SearchResults({ results, selectedIndex, query }) {
12
22
  return (_jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, paddingY: 1, minHeight: 5, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: ["[", search.resultsTitle, "] (", results.length, ")"] }) }), results.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: search.noResults })) : (results.slice(0, 10).map((task, index) => {
13
23
  const isSelected = index === selectedIndex;
14
24
  const shortId = task.id.slice(0, 8);
15
- const displayLabel = task.isProject ? i18n.tui.keyBar.project : i18n.status[task.status];
25
+ let displayLabel;
26
+ if (task.isProject) {
27
+ displayLabel = i18n.tui.keyBar.project;
28
+ }
29
+ else if (viewMode === 'kanban') {
30
+ const kanbanColumn = getKanbanColumn(task.status);
31
+ displayLabel = i18n.kanban[kanbanColumn];
32
+ }
33
+ else {
34
+ displayLabel = i18n.status[task.status];
35
+ }
16
36
  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, _jsxs(Text, { color: theme.colors.textMuted, children: [" (", displayLabel, ")"] }), task.waitingFor && (_jsxs(Text, { color: theme.colors.statusWaiting, children: [" - ", task.waitingFor] }))] }) }, task.id));
17
37
  })), results.length > 10 && (_jsxs(Text, { color: theme.colors.textMuted, italic: true, children: ["... and ", results.length - 10, " more"] }))] }));
18
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "floq",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Floq - Getting Things Done Task Manager with MS-DOS style themes",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",