floq 0.7.0 → 0.8.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/dist/changelog.js CHANGED
@@ -1,27 +1,44 @@
1
1
  import { readFileSync } from 'fs';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { dirname, join } from 'path';
4
+ import { getLocale } from './config.js';
4
5
  function findChangelogPath() {
5
6
  // Get the directory of the current module
6
7
  const currentFile = fileURLToPath(import.meta.url);
7
8
  const currentDir = dirname(currentFile);
8
- // Try to find CHANGELOG.md relative to the package root
9
+ const locale = getLocale();
10
+ const filename = locale === 'ja' ? 'CHANGELOG.ja.md' : 'CHANGELOG.md';
11
+ const fallbackFilename = 'CHANGELOG.md';
12
+ // Try to find localized CHANGELOG relative to the package root
9
13
  // From dist/ or src/, go up one level
10
- const possiblePaths = [
11
- join(currentDir, '..', 'CHANGELOG.md'),
12
- join(currentDir, '..', '..', 'CHANGELOG.md'),
13
- join(process.cwd(), 'CHANGELOG.md'),
14
+ const basePaths = [
15
+ join(currentDir, '..'),
16
+ join(currentDir, '..', '..'),
17
+ process.cwd(),
14
18
  ];
15
- for (const path of possiblePaths) {
19
+ // First, try to find the localized version
20
+ for (const basePath of basePaths) {
21
+ const localizedPath = join(basePath, filename);
16
22
  try {
17
- readFileSync(path, 'utf-8');
18
- return path;
23
+ readFileSync(localizedPath, 'utf-8');
24
+ return localizedPath;
19
25
  }
20
26
  catch {
21
27
  // continue to next path
22
28
  }
23
29
  }
24
- return possiblePaths[0]; // Return first path as fallback
30
+ // Fall back to default CHANGELOG.md
31
+ for (const basePath of basePaths) {
32
+ const fallbackPath = join(basePath, fallbackFilename);
33
+ try {
34
+ readFileSync(fallbackPath, 'utf-8');
35
+ return fallbackPath;
36
+ }
37
+ catch {
38
+ // continue to next path
39
+ }
40
+ }
41
+ return join(basePaths[0], fallbackFilename); // Return first path as fallback
25
42
  }
26
43
  export function parseChangelog() {
27
44
  const changelogPath = findChangelogPath();
package/dist/cli.js CHANGED
@@ -20,7 +20,14 @@ program
20
20
  // Default command - launch TUI
21
21
  program
22
22
  .action(() => {
23
- render(React.createElement(App));
23
+ // Enter alternate screen buffer (like btop/vim)
24
+ process.stdout.write('\x1b[?1049h');
25
+ process.stdout.write('\x1b[H'); // Move cursor to top-left
26
+ const { waitUntilExit } = render(React.createElement(App));
27
+ waitUntilExit().then(() => {
28
+ // Leave alternate screen buffer
29
+ process.stdout.write('\x1b[?1049l');
30
+ });
24
31
  });
25
32
  // Add task
26
33
  program
package/dist/config.d.ts CHANGED
@@ -14,6 +14,7 @@ export interface Config {
14
14
  turso?: TursoConfig;
15
15
  contexts?: string[];
16
16
  splashDuration?: number;
17
+ contextFilter?: string | null;
17
18
  }
18
19
  export declare function loadConfig(): Config;
19
20
  export declare function saveConfig(updates: Partial<Config>): void;
@@ -33,3 +34,5 @@ export declare function addContext(context: string): boolean;
33
34
  export declare function removeContext(context: string): boolean;
34
35
  export declare function getSplashDuration(): number;
35
36
  export declare function setSplashDuration(duration: number): void;
37
+ export declare function getContextFilter(): string | null;
38
+ export declare function setContextFilter(contextFilter: string | null): void;
package/dist/config.js CHANGED
@@ -151,3 +151,11 @@ export function setSplashDuration(duration) {
151
151
  // Allow -1 (wait for key), 0 (disabled), or positive values
152
152
  saveConfig({ splashDuration: duration >= 0 ? duration : -1 });
153
153
  }
154
+ export function getContextFilter() {
155
+ const config = loadConfig();
156
+ // undefined means not set (default to null = all)
157
+ return config.contextFilter === undefined ? null : config.contextFilter;
158
+ }
159
+ export function setContextFilter(contextFilter) {
160
+ saveConfig({ contextFilter });
161
+ }
package/dist/index.js CHANGED
File without changes
package/dist/ui/App.js CHANGED
@@ -17,7 +17,7 @@ import { LanguageSelector } from './LanguageSelector.js';
17
17
  import { getDb, schema } from '../db/index.js';
18
18
  import { t, fmt } from '../i18n/index.js';
19
19
  import { ThemeProvider, useTheme, getTheme } from './theme/index.js';
20
- import { getThemeName, getViewMode, setThemeName, setViewMode, setLocale, isTursoEnabled, getContexts, addContext, getSplashDuration } from '../config.js';
20
+ import { getThemeName, getViewMode, setThemeName, setViewMode, setLocale, isTursoEnabled, getContexts, addContext, getSplashDuration, getContextFilter, setContextFilter as saveContextFilter } from '../config.js';
21
21
  import { KanbanBoard } from './components/KanbanBoard.js';
22
22
  import { KanbanDQ } from './components/KanbanDQ.js';
23
23
  import { KanbanMario } from './components/KanbanMario.js';
@@ -101,8 +101,12 @@ function AppContent({ onOpenSettings }) {
101
101
  const [searchQuery, setSearchQuery] = useState('');
102
102
  const [searchResults, setSearchResults] = useState([]);
103
103
  const [searchResultIndex, setSearchResultIndex] = useState(0);
104
- // Context filter state
105
- const [contextFilter, setContextFilter] = useState(null); // null = all, '' = no context, string = specific context
104
+ // Context filter state - load from config for persistence across sessions/terminals
105
+ const [contextFilter, setContextFilterState] = useState(() => getContextFilter());
106
+ const setContextFilter = useCallback((value) => {
107
+ setContextFilterState(value);
108
+ saveContextFilter(value);
109
+ }, []);
106
110
  const [contextSelectIndex, setContextSelectIndex] = useState(0);
107
111
  const [availableContexts, setAvailableContexts] = useState([]);
108
112
  const i18n = t();
@@ -7,7 +7,7 @@ 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 } from '../../config.js';
10
+ import { isTursoEnabled, getContexts, addContext, getLocale, getContextFilter, setContextFilter as saveContextFilter } from '../../config.js';
11
11
  import { useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from '../history/index.js';
12
12
  import { SearchBar } from './SearchBar.js';
13
13
  import { SearchResults } from './SearchResults.js';
@@ -139,7 +139,12 @@ export function GtdDQ({ onOpenSettings }) {
139
139
  const [taskToWaiting, setTaskToWaiting] = useState(null);
140
140
  const [taskToDelete, setTaskToDelete] = useState(null);
141
141
  const [projectProgress, setProjectProgress] = useState({});
142
- const [contextFilter, setContextFilter] = useState(null);
142
+ // Context filter state - load from config for persistence across sessions/terminals
143
+ const [contextFilter, setContextFilterState] = useState(() => getContextFilter());
144
+ const setContextFilter = useCallback((value) => {
145
+ setContextFilterState(value);
146
+ saveContextFilter(value);
147
+ }, []);
143
148
  const [contextSelectIndex, setContextSelectIndex] = useState(0);
144
149
  const [availableContexts, setAvailableContexts] = useState([]);
145
150
  const [searchQuery, setSearchQuery] = useState('');
@@ -894,6 +899,9 @@ export function GtdDQ({ onOpenSettings }) {
894
899
  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.inbox.length + tasks.next.length }), _jsxs(Text, { color: theme.colors.textMuted, children: ["/", tasks.inbox.length + tasks.next.length + tasks.waiting.length + tasks.someday.length] }), _jsx(Text, { color: theme.colors.text, children: " MP " }), _jsx(Text, { bold: true, color: theme.colors.statusWaiting, children: tasks.projects.length }), _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] }))] }), _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) => {
895
900
  const label = ctx === 'all' ? 'All' : ctx === 'none' ? 'No context' : `@${ctx}`;
896
901
  return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '▶ ' : ' ', label] }, ctx));
902
+ }) })] })) : mode === 'set-context' && currentTasks.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.setContext || 'Set context for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['clear', ...availableContexts, 'new'].map((ctx, index) => {
903
+ const label = ctx === 'clear' ? (i18n.tui.context?.none || 'Clear context') : ctx === 'new' ? (i18n.tui.context?.addNew || 'New context...') : `@${ctx}`;
904
+ return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '▶ ' : ' ', label] }, ctx));
897
905
  }) })] })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '▶ ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : (mode === 'task-detail' || mode === 'add-comment') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(TitledBoxInline, { title: i18n.tui.taskDetailTitle || 'Task Details', width: terminalWidth - 4, minHeight: 4, isActive: true, 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, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus || 'Status', ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(TitledBoxInline, { title: `${i18n.tui.comments || 'Comments'} (${taskComments.length})`, width: terminalWidth - 4, minHeight: 5, isActive: mode === 'task-detail', children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
898
906
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
899
907
  return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.textMuted, children: [isSelected ? '▶ ' : ' ', "[", comment.createdAt.toLocaleString(), "]"] }), _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [' ', comment.content] })] }, comment.id));
@@ -7,7 +7,7 @@ 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 } from '../../config.js';
10
+ import { isTursoEnabled, getContexts, addContext, getContextFilter, setContextFilter as saveContextFilter } from '../../config.js';
11
11
  import { VERSION } from '../../version.js';
12
12
  import { useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from '../history/index.js';
13
13
  import { SearchBar } from './SearchBar.js';
@@ -81,7 +81,12 @@ export function GtdMario({ onOpenSettings }) {
81
81
  const [taskToWaiting, setTaskToWaiting] = useState(null);
82
82
  const [taskToDelete, setTaskToDelete] = useState(null);
83
83
  const [projectProgress, setProjectProgress] = useState({});
84
- const [contextFilter, setContextFilter] = useState(null);
84
+ // Context filter state - load from config for persistence across sessions/terminals
85
+ const [contextFilter, setContextFilterState] = useState(() => getContextFilter());
86
+ const setContextFilter = useCallback((value) => {
87
+ setContextFilterState(value);
88
+ saveContextFilter(value);
89
+ }, []);
85
90
  const [contextSelectIndex, setContextSelectIndex] = useState(0);
86
91
  const [availableContexts, setAvailableContexts] = useState([]);
87
92
  const [searchQuery, setSearchQuery] = useState('');
@@ -798,6 +803,9 @@ export function GtdMario({ onOpenSettings }) {
798
803
  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.inbox.length + tasks.next.length + tasks.waiting.length + tasks.someday.length + tasks.projects.length).toString().padStart(2, '0') }), _jsxs(Text, { color: theme.colors.textMuted, children: [" FLOQ v", VERSION] }), _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(Text, { color: theme.colors.secondary, children: "TIME " }), _jsx(Text, { color: theme.colors.text, children: new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' }) })] })] }), 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) => {
799
804
  const label = ctx === 'all' ? 'All' : ctx === 'none' ? 'No context' : `@${ctx}`;
800
805
  return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '🍄 ' : ' ', label] }, ctx));
806
+ }) })] })) : mode === 'set-context' && currentTasks.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.setContext || 'Set context for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['clear', ...availableContexts, 'new'].map((ctx, index) => {
807
+ const label = ctx === 'clear' ? (i18n.tui.context?.none || 'Clear context') : ctx === 'new' ? (i18n.tui.context?.addNew || 'New context...') : `@${ctx}`;
808
+ return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '🍄 ' : ' ', label] }, ctx));
801
809
  }) })] })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '🍄 ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : (mode === 'task-detail' || mode === 'add-comment') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(MarioBoxInline, { title: i18n.tui.taskDetailTitle || 'Task Details', width: terminalWidth - 4, minHeight: 4, isActive: true, 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, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus || 'Status', ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(MarioBoxInline, { title: `${i18n.tui.comments || 'Comments'} (${taskComments.length})`, width: terminalWidth - 4, minHeight: 5, isActive: mode === 'task-detail', children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
802
810
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
803
811
  return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.textMuted, children: [isSelected ? '🍄 ' : ' ', "[", comment.createdAt.toLocaleString(), "]"] }), _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [' ', comment.content] })] }, comment.id));
@@ -12,7 +12,7 @@ import { SearchResults } from './SearchResults.js';
12
12
  import { getDb, schema } from '../../db/index.js';
13
13
  import { t, fmt } from '../../i18n/index.js';
14
14
  import { useTheme } from '../theme/index.js';
15
- import { isTursoEnabled, getContexts, addContext } from '../../config.js';
15
+ import { isTursoEnabled, getContexts, addContext, getContextFilter, setContextFilter as saveContextFilter } from '../../config.js';
16
16
  import { VERSION } from '../../version.js';
17
17
  import { useHistory, CreateTaskCommand, MoveTaskCommand, LinkTaskCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from '../history/index.js';
18
18
  const COLUMNS = ['todo', 'doing', 'done'];
@@ -43,8 +43,12 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
43
43
  const [searchQuery, setSearchQuery] = useState('');
44
44
  const [searchResults, setSearchResults] = useState([]);
45
45
  const [searchResultIndex, setSearchResultIndex] = useState(0);
46
- // Context filter state
47
- const [contextFilter, setContextFilter] = useState(null);
46
+ // Context filter state - load from config for persistence across sessions/terminals
47
+ const [contextFilter, setContextFilterState] = useState(() => getContextFilter());
48
+ const setContextFilter = useCallback((value) => {
49
+ setContextFilterState(value);
50
+ saveContextFilter(value);
51
+ }, []);
48
52
  const [contextSelectIndex, setContextSelectIndex] = useState(0);
49
53
  const [availableContexts, setAvailableContexts] = useState([]);
50
54
  const i18n = t();
@@ -7,7 +7,7 @@ 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 } from '../../config.js';
10
+ import { isTursoEnabled, getContexts, addContext, getLocale, getContextFilter, setContextFilter as saveContextFilter } from '../../config.js';
11
11
  import { useHistory, CreateTaskCommand, MoveTaskCommand, } from '../history/index.js';
12
12
  import { SearchBar } from './SearchBar.js';
13
13
  import { SearchResults } from './SearchResults.js';
@@ -113,7 +113,12 @@ export function KanbanDQ({ onOpenSettings }) {
113
113
  const [selectedTask, setSelectedTask] = useState(null);
114
114
  const [taskComments, setTaskComments] = useState([]);
115
115
  const [selectedCommentIndex, setSelectedCommentIndex] = useState(0);
116
- const [contextFilter, setContextFilter] = useState(null);
116
+ // Context filter state - load from config for persistence across sessions/terminals
117
+ const [contextFilter, setContextFilterState] = useState(() => getContextFilter());
118
+ const setContextFilter = useCallback((value) => {
119
+ setContextFilterState(value);
120
+ saveContextFilter(value);
121
+ }, []);
117
122
  const [contextSelectIndex, setContextSelectIndex] = useState(0);
118
123
  const [availableContexts, setAvailableContexts] = useState([]);
119
124
  const [searchQuery, setSearchQuery] = useState('');
@@ -7,7 +7,7 @@ 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 } from '../../config.js';
10
+ import { isTursoEnabled, getContexts, addContext, getContextFilter, setContextFilter as saveContextFilter } from '../../config.js';
11
11
  import { VERSION } from '../../version.js';
12
12
  import { useHistory, CreateTaskCommand, MoveTaskCommand, } from '../history/index.js';
13
13
  import { SearchBar } from './SearchBar.js';
@@ -35,7 +35,12 @@ export function KanbanMario({ onOpenSettings }) {
35
35
  const [selectedTask, setSelectedTask] = useState(null);
36
36
  const [taskComments, setTaskComments] = useState([]);
37
37
  const [selectedCommentIndex, setSelectedCommentIndex] = useState(0);
38
- const [contextFilter, setContextFilter] = useState(null);
38
+ // Context filter state - load from config for persistence across sessions/terminals
39
+ const [contextFilter, setContextFilterState] = useState(() => getContextFilter());
40
+ const setContextFilter = useCallback((value) => {
41
+ setContextFilterState(value);
42
+ saveContextFilter(value);
43
+ }, []);
39
44
  const [contextSelectIndex, setContextSelectIndex] = useState(0);
40
45
  const [availableContexts, setAvailableContexts] = useState([]);
41
46
  const [searchQuery, setSearchQuery] = useState('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "floq",
3
- "version": "0.7.0",
3
+ "version": "0.8.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",
@@ -1,36 +0,0 @@
1
- import React from 'react';
2
- interface DQLayoutProps {
3
- title: string;
4
- subtitle?: string;
5
- menuTitle: string;
6
- menuItems: Array<{
7
- label: string;
8
- count?: number;
9
- isActive: boolean;
10
- }>;
11
- onMenuSelect?: (index: number) => void;
12
- contentTitle: string;
13
- children: React.ReactNode;
14
- statusTitle?: string;
15
- statusItems?: Array<{
16
- label: string;
17
- value: string;
18
- }>;
19
- footer?: string;
20
- }
21
- /**
22
- * Dragon Quest style multi-window layout
23
- *
24
- * ╭────────────────────────────────────────────╮
25
- * │ FLOQ - GTD Manager │
26
- * ╰────────────────────────────────────────────╯
27
- * ╭─ コマンド ─╮ ╭─ Inbox ──────────────────────╮
28
- * │ ▶ Inbox │ │ ▶ タスク1 │
29
- * │ 次 │ │ タスク2 │
30
- * │ 待ち │ │ タスク3 │
31
- * │ いつか │ ╰────────────────────────────────╯
32
- * │ 完了 │
33
- * ╰───────────╯
34
- */
35
- export declare function DQLayout({ title, subtitle, menuTitle, menuItems, contentTitle, children, statusTitle, statusItems, footer, }: DQLayoutProps): React.ReactElement;
36
- export {};
@@ -1,53 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import { useTheme } from '../theme/index.js';
4
- /**
5
- * Dragon Quest style multi-window layout
6
- *
7
- * ╭────────────────────────────────────────────╮
8
- * │ FLOQ - GTD Manager │
9
- * ╰────────────────────────────────────────────╯
10
- * ╭─ コマンド ─╮ ╭─ Inbox ──────────────────────╮
11
- * │ ▶ Inbox │ │ ▶ タスク1 │
12
- * │ 次 │ │ タスク2 │
13
- * │ 待ち │ │ タスク3 │
14
- * │ いつか │ ╰────────────────────────────────╯
15
- * │ 完了 │
16
- * ╰───────────╯
17
- */
18
- export function DQLayout({ title, subtitle, menuTitle, menuItems, contentTitle, children, statusTitle, statusItems, footer, }) {
19
- const theme = useTheme();
20
- const borderColor = theme.colors.border;
21
- const activeColor = theme.colors.borderActive;
22
- // Render a window with title on border
23
- const renderWindow = (windowTitle, content, options = {}) => {
24
- const { width, minHeight = 3, active = false, flexGrow } = options;
25
- const color = active ? activeColor : borderColor;
26
- const titleDisplay = ` ${windowTitle} `;
27
- const minW = width || 20;
28
- return (_jsxs(Box, { flexDirection: "column", width: width, minHeight: minHeight, flexGrow: flexGrow, children: [_jsxs(Box, { children: [_jsx(Text, { color: color, children: "\u256D\u2500\u2500" }), _jsx(Text, { color: theme.colors.text, bold: true, children: titleDisplay }), _jsxs(Text, { color: color, children: ['─'.repeat(Math.max(minW - titleDisplay.length - 6, 2)), "\u256E"] })] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [_jsx(Text, { color: color, children: "\u2502 " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: content }), _jsx(Text, { color: color, children: " \u2502" })] }) }), _jsx(Box, { children: _jsxs(Text, { color: color, children: ["\u2570", '─'.repeat(Math.max(minW - 2, 2)), "\u256F"] }) })] }));
29
- };
30
- // Render header window
31
- const renderHeader = () => {
32
- const headerWidth = 50;
33
- const padding = Math.floor((headerWidth - title.length - 4) / 2);
34
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { children: _jsxs(Text, { color: borderColor, children: ["\u256D", '─'.repeat(headerWidth - 2), "\u256E"] }) }), _jsxs(Box, { children: [_jsx(Text, { color: borderColor, children: "\u2502" }), _jsxs(Text, { color: theme.colors.primary, bold: true, children: [' '.repeat(padding), title, ' '.repeat(padding)] }), _jsx(Text, { color: borderColor, children: "\u2502" })] }), subtitle && (_jsxs(Box, { children: [_jsx(Text, { color: borderColor, children: "\u2502" }), _jsxs(Text, { color: theme.colors.textMuted, children: [' '.repeat(Math.floor((headerWidth - subtitle.length - 4) / 2)), subtitle] }), _jsx(Text, { color: borderColor, children: "\u2502" })] })), _jsx(Box, { children: _jsxs(Text, { color: borderColor, children: ["\u2570", '─'.repeat(headerWidth - 2), "\u256F"] }) })] }));
35
- };
36
- // Render menu items
37
- const renderMenuContent = () => (_jsx(_Fragment, { children: menuItems.map((item, index) => (_jsx(Box, { children: _jsxs(Text, { color: item.isActive ? theme.colors.textSelected : theme.colors.text, bold: item.isActive, children: [item.isActive ? theme.style.selectedPrefix : theme.style.unselectedPrefix, item.label, item.count !== undefined && ` (${item.count})`] }) }, index))) }));
38
- // Render status window
39
- const renderStatus = () => {
40
- if (!statusTitle || !statusItems)
41
- return null;
42
- return (_jsx(Box, { marginTop: 1, children: renderWindow(statusTitle, _jsx(_Fragment, { children: statusItems.map((item, index) => (_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.textMuted, children: [item.label, ": "] }), _jsx(Text, { color: theme.colors.accent, children: item.value })] }, index))) }), { width: 20, minHeight: statusItems.length + 2 }) }));
43
- };
44
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [renderHeader(), _jsxs(Box, { flexDirection: "row", children: [_jsxs(Box, { flexDirection: "column", marginRight: 1, children: [renderWindow(menuTitle, renderMenuContent(), {
45
- width: 18,
46
- minHeight: menuItems.length + 2,
47
- active: true,
48
- }), renderStatus()] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: renderWindow(contentTitle, children, {
49
- minHeight: 12,
50
- active: true,
51
- flexGrow: 1,
52
- }) })] }), footer && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textMuted, children: footer }) }))] }));
53
- }
@@ -1,53 +0,0 @@
1
- import React from 'react';
2
- import type { Task } from '../../db/schema.js';
3
- import type { ProjectProgress } from './TaskItem.js';
4
- interface DQTaskListProps {
5
- tasks: Task[];
6
- selectedIndex: number;
7
- emptyMessage: string;
8
- showProject?: boolean;
9
- getParentProject?: (parentId: string | null) => Task | undefined;
10
- projectProgress?: Record<string, ProjectProgress>;
11
- isProjectTab?: boolean;
12
- }
13
- /**
14
- * Dragon Quest style task list inside a window
15
- */
16
- export declare function DQTaskList({ tasks, selectedIndex, emptyMessage, showProject, getParentProject, projectProgress, isProjectTab, }: DQTaskListProps): React.ReactElement;
17
- interface DQWindowFrameProps {
18
- title: string;
19
- children: React.ReactNode;
20
- width?: number | string;
21
- minHeight?: number;
22
- active?: boolean;
23
- flexGrow?: number;
24
- }
25
- /**
26
- * Dragon Quest style window frame with title on border
27
- */
28
- export declare function DQWindowFrame({ title, children, width, minHeight, active, flexGrow, }: DQWindowFrameProps): React.ReactElement;
29
- interface DQMenuProps {
30
- title: string;
31
- items: Array<{
32
- key: string;
33
- label: string;
34
- count: number;
35
- isActive: boolean;
36
- }>;
37
- width?: number;
38
- }
39
- /**
40
- * Dragon Quest style menu (left side panel)
41
- */
42
- export declare function DQMenu({ title, items, width }: DQMenuProps): React.ReactElement;
43
- interface DQHeaderProps {
44
- title: string;
45
- version: string;
46
- dbMode: string;
47
- contextFilter?: string | null;
48
- }
49
- /**
50
- * Dragon Quest style header window
51
- */
52
- export declare function DQHeader({ title, version, dbMode, contextFilter }: DQHeaderProps): React.ReactElement;
53
- export {};
@@ -1,48 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import { useTheme } from '../theme/index.js';
4
- /**
5
- * Dragon Quest style task list inside a window
6
- */
7
- export function DQTaskList({ tasks, selectedIndex, emptyMessage, showProject = false, getParentProject, projectProgress, isProjectTab = false, }) {
8
- const theme = useTheme();
9
- if (tasks.length === 0) {
10
- return (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: emptyMessage }));
11
- }
12
- return (_jsx(_Fragment, { children: tasks.map((task, index) => {
13
- const isSelected = index === selectedIndex;
14
- const parentProject = showProject && getParentProject ? getParentProject(task.parentId) : undefined;
15
- const progress = isProjectTab && projectProgress ? projectProgress[task.id] : undefined;
16
- return (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix, task.title, task.waitingFor && (_jsxs(Text, { color: theme.colors.statusWaiting, children: [" [", task.waitingFor, "]"] })), task.context && (_jsxs(Text, { color: theme.colors.accent, children: [" @", task.context] })), parentProject && (_jsxs(Text, { color: theme.colors.textMuted, children: [" \u2190 ", parentProject.title] })), progress && (_jsxs(Text, { color: theme.colors.textMuted, children: [" [", progress.completed, "/", progress.total, "]"] }))] }) }, task.id));
17
- }) }));
18
- }
19
- /**
20
- * Dragon Quest style window frame with title on border
21
- */
22
- export function DQWindowFrame({ title, children, width, minHeight = 10, active = false, flexGrow, }) {
23
- const theme = useTheme();
24
- const borderColor = active ? theme.colors.borderActive : theme.colors.border;
25
- const titleText = ` ${title} `;
26
- return (_jsxs(Box, { flexDirection: "column", width: width, minHeight: minHeight, flexGrow: flexGrow, children: [_jsxs(Text, { color: borderColor, children: ["\u256D\u2500\u2500", _jsx(Text, { color: theme.colors.text, bold: true, children: titleText }), _jsx(Text, { color: borderColor, children: '─'.repeat(30) })] }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [_jsx(Text, { color: borderColor, children: "\u2502 " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: children }), _jsx(Text, { color: borderColor, children: " \u2502" })] }), _jsxs(Text, { color: borderColor, children: ["\u2570", '─'.repeat(titleText.length + 32), "\u256F"] })] }));
27
- }
28
- /**
29
- * Dragon Quest style menu (left side panel)
30
- */
31
- export function DQMenu({ title, items, width = 16 }) {
32
- const theme = useTheme();
33
- const borderColor = theme.colors.border;
34
- const titleText = ` ${title} `;
35
- const innerWidth = width - 4;
36
- return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(Text, { color: borderColor, children: ["\u256D\u2500", _jsx(Text, { color: theme.colors.text, bold: true, children: titleText }), _jsxs(Text, { color: borderColor, children: ['─'.repeat(Math.max(innerWidth - titleText.length, 0)), "\u256E"] })] }), items.map((item) => (_jsxs(Box, { children: [_jsx(Text, { color: borderColor, children: "\u2502" }), _jsxs(Text, { color: item.isActive ? theme.colors.textSelected : theme.colors.text, bold: item.isActive, children: [item.isActive ? theme.style.selectedPrefix : theme.style.unselectedPrefix, item.label] }), _jsxs(Text, { color: theme.colors.textMuted, children: ["(", item.count, ")"] }), _jsx(Text, { color: borderColor, children: "\u2502" })] }, item.key))), _jsxs(Text, { color: borderColor, children: ["\u2570", '─'.repeat(innerWidth + 2), "\u256F"] })] }));
37
- }
38
- /**
39
- * Dragon Quest style header window
40
- */
41
- export function DQHeader({ title, version, dbMode, contextFilter }) {
42
- const theme = useTheme();
43
- const borderColor = theme.colors.border;
44
- const headerText = `${title} ver.${version}`;
45
- const width = 50;
46
- const padding = Math.floor((width - headerText.length - 2) / 2);
47
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: borderColor, children: ["\u256D", '─'.repeat(width - 2), "\u256E"] }), _jsxs(Box, { children: [_jsx(Text, { color: borderColor, children: "\u2502" }), _jsxs(Text, { color: theme.colors.primary, bold: true, children: [' '.repeat(padding), headerText, ' '.repeat(padding)] }), _jsx(Text, { color: borderColor, children: "\u2502" })] }), _jsxs(Box, { children: [_jsx(Text, { color: borderColor, children: "\u2502" }), _jsxs(Text, { children: [' '.repeat(Math.floor((width - 20) / 2)), _jsxs(Text, { color: theme.colors.textMuted, children: ["[", dbMode, "]"] }), contextFilter !== null && contextFilter !== undefined && (_jsxs(Text, { color: theme.colors.accent, children: [" @", contextFilter === '' ? 'none' : contextFilter] }))] }), _jsx(Text, { color: borderColor, children: "\u2502" })] }), _jsxs(Text, { color: borderColor, children: ["\u2570", '─'.repeat(width - 2), "\u256F"] })] }));
48
- }
@@ -1,19 +0,0 @@
1
- import React from 'react';
2
- interface DQWindowProps {
3
- title?: string;
4
- children: React.ReactNode;
5
- width?: number | string;
6
- height?: number | string;
7
- minHeight?: number;
8
- minWidth?: number;
9
- active?: boolean;
10
- }
11
- /**
12
- * Dragon Quest style window with title on the border
13
- *
14
- * ╭─ Title ──────────╮
15
- * │ Content here │
16
- * ╰──────────────────╯
17
- */
18
- export declare function DQWindow({ title, children, width, height, minHeight, minWidth, active, }: DQWindowProps): React.ReactElement;
19
- export {};
@@ -1,33 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import { useTheme } from '../theme/index.js';
4
- /**
5
- * Dragon Quest style window with title on the border
6
- *
7
- * ╭─ Title ──────────╮
8
- * │ Content here │
9
- * ╰──────────────────╯
10
- */
11
- export function DQWindow({ title, children, width, height, minHeight, minWidth, active = false, }) {
12
- const theme = useTheme();
13
- const borderColor = active ? theme.colors.borderActive : theme.colors.border;
14
- // Build the top border with title embedded
15
- const renderTopBorder = (contentWidth) => {
16
- if (!title) {
17
- // No title - just return standard corner
18
- return null;
19
- }
20
- const titleText = ` ${title} `;
21
- const titleLen = titleText.length;
22
- // Calculate dashes needed (minimum 2 on each side of title)
23
- const availableWidth = Math.max(contentWidth - titleLen - 4, 0);
24
- const leftDashes = 2;
25
- const rightDashes = Math.max(availableWidth - leftDashes, 2);
26
- return (_jsxs(Box, { children: [_jsxs(Text, { color: borderColor, children: ["\u256D", '─'.repeat(leftDashes)] }), _jsx(Text, { color: theme.colors.text, bold: true, children: titleText }), _jsxs(Text, { color: borderColor, children: ['─'.repeat(rightDashes), "\u256E"] })] }));
27
- };
28
- const renderBottomBorder = (contentWidth) => {
29
- return (_jsx(Box, { children: _jsxs(Text, { color: borderColor, children: ["\u2570", '─'.repeat(contentWidth), "\u256F"] }) }));
30
- };
31
- // For DQ style, we render custom borders
32
- return (_jsxs(Box, { flexDirection: "column", width: width, height: height, minHeight: minHeight, minWidth: minWidth, children: [title && renderTopBorder(typeof minWidth === 'number' ? minWidth - 2 : 20), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: borderColor, children: "\u2502" }), _jsx(Box, { flexDirection: "column", flexGrow: 1, paddingX: 1, children: children }), _jsx(Text, { color: borderColor, children: "\u2502" })] }), renderBottomBorder(typeof minWidth === 'number' ? minWidth - 2 : 20)] }));
33
- }
@@ -1,8 +0,0 @@
1
- import React from 'react';
2
- interface FullScreenBoxProps {
3
- children: React.ReactNode;
4
- backgroundColor?: string;
5
- padding?: number;
6
- }
7
- export declare function FullScreenBox({ children, backgroundColor, padding }: FullScreenBoxProps): React.ReactElement;
8
- export {};
@@ -1,10 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, useStdout } from 'ink';
3
- export function FullScreenBox({ children, backgroundColor, padding = 1 }) {
4
- const { stdout } = useStdout();
5
- const width = stdout?.columns || 80;
6
- const height = stdout?.rows || 24;
7
- // 背景色で画面全体を埋めるために、各行に空白を追加
8
- const fillLine = backgroundColor ? ' '.repeat(width) : '';
9
- return (_jsxs(Box, { flexDirection: "column", width: width, minHeight: height, padding: padding, ...(backgroundColor ? { backgroundColor } : {}), children: [children, backgroundColor && (_jsx(Box, { flexGrow: 1, width: width, children: _jsx(Box, { backgroundColor: backgroundColor, width: width }) }))] }));
10
- }