floq 0.2.1 → 0.2.3

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
@@ -64,6 +64,15 @@ floq
64
64
  | `?` | ヘルプ |
65
65
  | `q` | 終了 |
66
66
 
67
+ #### 検索
68
+
69
+ | キー | アクション |
70
+ |------|-----------|
71
+ | `/` | 検索モード開始 |
72
+ | `↑/↓` または `Ctrl+j/k` | 検索結果をナビゲート |
73
+ | `Enter` | 選択したタスクのタブに移動して選択 |
74
+ | `Esc` | キャンセル |
75
+
67
76
  #### プロジェクト詳細画面
68
77
 
69
78
  | キー | アクション |
@@ -228,7 +237,7 @@ floq config turso --disable
228
237
 
229
238
  ## テーマ
230
239
 
231
- 16種類のテーマが利用可能。`floq config theme` でインタラクティブに選択(j/kで移動)。
240
+ 26種類のテーマが利用可能。`floq config theme` でインタラクティブに選択(j/kで移動)。
232
241
 
233
242
  | テーマ | 説明 |
234
243
  |--------|------|
@@ -248,6 +257,16 @@ floq config turso --disable
248
257
  | `synthwave` | ネオン80sスタイル |
249
258
  | `paper` | 紙とインク風ライト |
250
259
  | `coffee` | 暖かみのある茶系 |
260
+ | `nord` | 北欧風ブルーグレー |
261
+ | `dracula` | ダーク&ビビッドカラー |
262
+ | `monokai` | エディタ定番の鮮やかな配色 |
263
+ | `gruvbox` | レトロな暖色系ダーク |
264
+ | `tokyo-night` | 東京の夜景イメージ |
265
+ | `catppuccin` | パステル系モダン |
266
+ | `ocean` | 深海ブルー |
267
+ | `sakura` | 桜ピンク |
268
+ | `msx` | MSXコンピュータ(TMS9918) |
269
+ | `pc-98` | NEC PC-9801風 |
251
270
 
252
271
  > **注意**: 背景色はターミナルの設定に依存します。
253
272
 
package/README.md CHANGED
@@ -64,6 +64,15 @@ floq
64
64
  | `?` | Help |
65
65
  | `q` | Quit |
66
66
 
67
+ #### Search
68
+
69
+ | Key | Action |
70
+ |-----|--------|
71
+ | `/` | Start search mode |
72
+ | `↑/↓` or `Ctrl+j/k` | Navigate search results |
73
+ | `Enter` | Jump to selected task's tab and select it |
74
+ | `Esc` | Cancel search |
75
+
67
76
  #### Project Detail View
68
77
 
69
78
  | Key | Action |
@@ -228,7 +237,7 @@ floq config turso --disable
228
237
 
229
238
  ## Themes
230
239
 
231
- 16 themes available. Use `floq config theme` for interactive selection (j/k to navigate).
240
+ 26 themes available. Use `floq config theme` for interactive selection (j/k to navigate).
232
241
 
233
242
  | Theme | Description |
234
243
  |-------|-------------|
@@ -248,6 +257,16 @@ floq config turso --disable
248
257
  | `synthwave` | Neon 80s aesthetic |
249
258
  | `paper` | Light minimal theme |
250
259
  | `coffee` | Warm brown tones |
260
+ | `nord` | Arctic, north-bluish palette |
261
+ | `dracula` | Dark theme with vibrant colors |
262
+ | `monokai` | Classic editor vivid colors |
263
+ | `gruvbox` | Retro groove warm tones |
264
+ | `tokyo-night` | Tokyo night lights inspired |
265
+ | `catppuccin` | Soothing pastel theme |
266
+ | `ocean` | Deep sea blue theme |
267
+ | `sakura` | Cherry blossom pink |
268
+ | `msx` | MSX computer (TMS9918) |
269
+ | `pc-98` | NEC PC-9801 style |
251
270
 
252
271
  > **Note**: Background colors depend on your terminal settings.
253
272
 
package/dist/i18n/en.d.ts CHANGED
@@ -129,6 +129,10 @@ export declare const en: {
129
129
  taskDetail: string;
130
130
  addComment: string;
131
131
  searchTasks: string;
132
+ settings: string;
133
+ changeTheme: string;
134
+ changeViewMode: string;
135
+ changeLanguage: string;
132
136
  other: string;
133
137
  showHelp: string;
134
138
  quit: string;
@@ -156,6 +160,10 @@ export declare const en: {
156
160
  moveRight: string;
157
161
  moveLeft: string;
158
162
  searchTasks: string;
163
+ settings: string;
164
+ changeTheme: string;
165
+ changeViewMode: string;
166
+ changeLanguage: string;
159
167
  other: string;
160
168
  showHelp: string;
161
169
  quit: string;
@@ -274,6 +282,10 @@ export type HelpTranslations = {
274
282
  taskDetail: string;
275
283
  addComment: string;
276
284
  searchTasks: string;
285
+ settings: string;
286
+ changeTheme: string;
287
+ changeViewMode: string;
288
+ changeLanguage: string;
277
289
  other: string;
278
290
  showHelp: string;
279
291
  quit: string;
@@ -301,6 +313,10 @@ export type KanbanHelpTranslations = {
301
313
  moveRight: string;
302
314
  moveLeft: string;
303
315
  searchTasks: string;
316
+ settings: string;
317
+ changeTheme: string;
318
+ changeViewMode: string;
319
+ changeLanguage: string;
304
320
  other: string;
305
321
  showHelp: string;
306
322
  quit: string;
package/dist/i18n/en.js CHANGED
@@ -138,6 +138,10 @@ export const en = {
138
138
  taskDetail: 'View task details',
139
139
  addComment: 'Add comment',
140
140
  searchTasks: 'Search tasks',
141
+ settings: 'Settings',
142
+ changeTheme: 'Change theme',
143
+ changeViewMode: 'Change view mode',
144
+ changeLanguage: 'Change language',
141
145
  other: 'Other',
142
146
  showHelp: 'Show this help',
143
147
  quit: 'Quit',
@@ -167,6 +171,10 @@ export const en = {
167
171
  moveRight: 'Move task to next column',
168
172
  moveLeft: 'Move task to previous column',
169
173
  searchTasks: 'Search tasks',
174
+ settings: 'Settings',
175
+ changeTheme: 'Change theme',
176
+ changeViewMode: 'Change view mode',
177
+ changeLanguage: 'Change language',
170
178
  other: 'Other',
171
179
  showHelp: 'Show this help',
172
180
  quit: 'Quit',
@@ -190,7 +198,7 @@ export const en = {
190
198
  search: {
191
199
  prefix: '/',
192
200
  placeholder: 'Search tasks...',
193
- help: '(Enter to select, Ctrl+j/k to navigate, Esc to cancel)',
201
+ help: '(Enter to select, ↑/↓ or Ctrl+j/k to navigate, Esc to cancel)',
194
202
  noResults: 'No matching tasks',
195
203
  resultsTitle: 'Search Results',
196
204
  searchTasks: 'Search tasks',
package/dist/i18n/ja.js CHANGED
@@ -138,6 +138,10 @@ export const ja = {
138
138
  taskDetail: 'タスク詳細を表示',
139
139
  addComment: 'コメント追加',
140
140
  searchTasks: 'タスク検索',
141
+ settings: '設定',
142
+ changeTheme: 'テーマ変更',
143
+ changeViewMode: '表示モード変更',
144
+ changeLanguage: '言語変更',
141
145
  other: 'その他',
142
146
  showHelp: 'このヘルプを表示',
143
147
  quit: '終了',
@@ -167,6 +171,10 @@ export const ja = {
167
171
  moveRight: '次のカラムへ移動',
168
172
  moveLeft: '前のカラムへ戻す',
169
173
  searchTasks: 'タスク検索',
174
+ settings: '設定',
175
+ changeTheme: 'テーマ変更',
176
+ changeViewMode: '表示モード変更',
177
+ changeLanguage: '言語変更',
170
178
  other: 'その他',
171
179
  showHelp: 'このヘルプを表示',
172
180
  quit: '終了',
@@ -190,7 +198,7 @@ export const ja = {
190
198
  search: {
191
199
  prefix: '/',
192
200
  placeholder: 'タスクを検索...',
193
- help: '(Enterで選択, Ctrl+j/kで移動, Escでキャンセル)',
201
+ help: '(Enterで選択, ↑/↓またはCtrl+j/kで移動, Escでキャンセル)',
194
202
  noResults: '該当するタスクがありません',
195
203
  resultsTitle: '検索結果',
196
204
  searchTasks: 'タスク検索',
package/dist/ui/App.js CHANGED
@@ -10,19 +10,52 @@ import { FunctionKeyBar } from './components/FunctionKeyBar.js';
10
10
  import { SearchBar } from './components/SearchBar.js';
11
11
  import { SearchResults } from './components/SearchResults.js';
12
12
  import { SplashScreen } from './SplashScreen.js';
13
+ import { ThemeSelector } from './ThemeSelector.js';
14
+ import { ModeSelector } from './ModeSelector.js';
15
+ import { LanguageSelector } from './LanguageSelector.js';
13
16
  import { getDb, schema } from '../db/index.js';
14
17
  import { t, fmt } from '../i18n/index.js';
15
18
  import { ThemeProvider, useTheme } from './theme/index.js';
16
- import { getThemeName, getViewMode, isTursoEnabled } from '../config.js';
19
+ import { getThemeName, getViewMode, setThemeName, setViewMode, setLocale, isTursoEnabled } from '../config.js';
17
20
  import { KanbanBoard } from './components/KanbanBoard.js';
18
21
  import { VERSION } from '../version.js';
19
22
  const TABS = ['inbox', 'next', 'waiting', 'someday', 'projects', 'done'];
20
23
  export function App() {
21
- const themeName = getThemeName();
22
- const viewMode = getViewMode();
23
- return (_jsx(ThemeProvider, { themeName: themeName, children: viewMode === 'kanban' ? _jsx(KanbanBoard, {}) : _jsx(AppContent, {}) }));
24
+ const [themeName, setThemeNameState] = useState(getThemeName);
25
+ const [viewMode, setViewModeState] = useState(getViewMode);
26
+ const [settingsMode, setSettingsMode] = useState('none');
27
+ const [, forceUpdate] = useState({});
28
+ const handleThemeSelect = (theme) => {
29
+ setThemeName(theme);
30
+ setThemeNameState(theme);
31
+ setSettingsMode('none');
32
+ };
33
+ const handleModeSelect = (mode) => {
34
+ setViewMode(mode);
35
+ setViewModeState(mode);
36
+ setSettingsMode('none');
37
+ };
38
+ const handleLocaleSelect = (locale) => {
39
+ setLocale(locale);
40
+ setSettingsMode('none');
41
+ forceUpdate({});
42
+ };
43
+ const handleSettingsCancel = () => {
44
+ setSettingsMode('none');
45
+ };
46
+ // Settings selector screens
47
+ if (settingsMode === 'theme-select') {
48
+ return (_jsx(ThemeProvider, { themeName: themeName, children: _jsx(ThemeSelector, { onSelect: handleThemeSelect, onCancel: handleSettingsCancel }) }));
49
+ }
50
+ if (settingsMode === 'mode-select') {
51
+ return (_jsx(ThemeProvider, { themeName: themeName, children: _jsx(ModeSelector, { onSelect: handleModeSelect, onCancel: handleSettingsCancel }) }));
52
+ }
53
+ if (settingsMode === 'lang-select') {
54
+ return (_jsx(ThemeProvider, { themeName: themeName, children: _jsx(LanguageSelector, { onSelect: handleLocaleSelect, onCancel: handleSettingsCancel }) }));
55
+ }
56
+ return (_jsx(ThemeProvider, { themeName: themeName, children: viewMode === 'kanban' ? (_jsx(KanbanBoard, { onOpenSettings: setSettingsMode })) : (_jsx(AppContent, { onOpenSettings: setSettingsMode })) }));
24
57
  }
25
- function AppContent() {
58
+ function AppContent({ onOpenSettings }) {
26
59
  const theme = useTheme();
27
60
  const { exit } = useApp();
28
61
  const [mode, setMode] = useState('splash');
@@ -141,6 +174,18 @@ function AppContent() {
141
174
  setSearchResults(results);
142
175
  setSearchResultIndex(0);
143
176
  }, [searchTasks]);
177
+ // Navigate to a task from search results
178
+ const navigateToTask = useCallback((task) => {
179
+ const targetTab = task.isProject ? 'projects' : task.status;
180
+ const tabIndex = TABS.indexOf(targetTab);
181
+ const tabTasks = tasks[targetTab];
182
+ const taskIndex = tabTasks.findIndex(t => t.id === task.id);
183
+ if (tabIndex >= 0 && taskIndex >= 0) {
184
+ setCurrentListIndex(tabIndex);
185
+ setSelectedTaskIndex(taskIndex);
186
+ setMode('normal');
187
+ }
188
+ }, [tasks]);
144
189
  const addTask = useCallback(async (title, parentId) => {
145
190
  if (!title.trim())
146
191
  return;
@@ -191,9 +236,7 @@ function AppContent() {
191
236
  if (mode === 'search') {
192
237
  if (searchResults.length > 0) {
193
238
  const task = searchResults[searchResultIndex];
194
- setSelectedTask(task);
195
- loadTaskComments(task.id);
196
- setMode('task-detail');
239
+ navigateToTask(task);
197
240
  }
198
241
  else {
199
242
  setMode('normal');
@@ -312,12 +355,12 @@ function AppContent() {
312
355
  setMode('normal');
313
356
  return;
314
357
  }
315
- // Navigate search results with Ctrl+j/k or Ctrl+n/p
316
- if (key.ctrl && (input === 'j' || input === 'n')) {
358
+ // Navigate search results with arrow keys, Ctrl+j/k, or Ctrl+n/p
359
+ if (key.downArrow || (key.ctrl && (input === 'j' || input === 'n'))) {
317
360
  setSearchResultIndex((prev) => prev < searchResults.length - 1 ? prev + 1 : 0);
318
361
  return;
319
362
  }
320
- if (key.ctrl && (input === 'k' || input === 'p')) {
363
+ if (key.upArrow || (key.ctrl && (input === 'k' || input === 'p'))) {
321
364
  setSearchResultIndex((prev) => prev > 0 ? prev - 1 : Math.max(0, searchResults.length - 1));
322
365
  return;
323
366
  }
@@ -551,6 +594,21 @@ function AppContent() {
551
594
  setSearchResultIndex(0);
552
595
  return;
553
596
  }
597
+ // Settings: Theme selector
598
+ if (input === 'T') {
599
+ onOpenSettings('theme-select');
600
+ return;
601
+ }
602
+ // Settings: Mode selector
603
+ if (input === 'V') {
604
+ onOpenSettings('mode-select');
605
+ return;
606
+ }
607
+ // Settings: Language selector
608
+ if (input === 'L') {
609
+ onOpenSettings('lang-select');
610
+ return;
611
+ }
554
612
  // Quit
555
613
  if (input === 'q' || (key.ctrl && input === 'c')) {
556
614
  exit();
@@ -725,7 +783,7 @@ function AppContent() {
725
783
  const tursoEnabled = isTursoEnabled();
726
784
  return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: formatTitle(i18n.tui.title) }), _jsx(Text, { color: theme.colors.textMuted, children: theme.name === 'modern' ? ` v${VERSION}` : ` VER ${VERSION}` }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: theme.name === 'modern'
727
785
  ? (tursoEnabled ? ' ☁️ turso' : ' 💾 local')
728
- : (tursoEnabled ? ' [DB]turso' : ' [DB]local') })] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
786
+ : (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') })] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
729
787
  const isActive = index === currentListIndex && mode !== 'project-detail';
730
788
  const count = tasks[tab].length;
731
789
  const label = `${index + 1}:${getTabLabel(tab)}(${count})`;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { type Locale } from '../config.js';
3
+ interface LanguageSelectorProps {
4
+ onSelect: (locale: Locale) => void;
5
+ onCancel: () => void;
6
+ }
7
+ export declare function LanguageSelector({ onSelect, onCancel }: LanguageSelectorProps): React.ReactElement;
8
+ export {};
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { getLocale } from '../config.js';
5
+ const VALID_LOCALES = ['en', 'ja'];
6
+ const localeDisplayNames = {
7
+ en: 'English',
8
+ ja: '日本語 (Japanese)',
9
+ };
10
+ export function LanguageSelector({ onSelect, onCancel }) {
11
+ const currentLocale = getLocale();
12
+ const initialIndex = VALID_LOCALES.indexOf(currentLocale);
13
+ const [selectedIndex, setSelectedIndex] = useState(initialIndex >= 0 ? initialIndex : 0);
14
+ useInput((input, key) => {
15
+ if (key.escape) {
16
+ onCancel();
17
+ return;
18
+ }
19
+ // j or down arrow: move down
20
+ if (input === 'j' || key.downArrow) {
21
+ setSelectedIndex((prev) => (prev < VALID_LOCALES.length - 1 ? prev + 1 : 0));
22
+ }
23
+ // k or up arrow: move up
24
+ if (input === 'k' || key.upArrow) {
25
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : VALID_LOCALES.length - 1));
26
+ }
27
+ // Enter: select
28
+ if (key.return) {
29
+ onSelect(VALID_LOCALES[selectedIndex]);
30
+ }
31
+ });
32
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Select language / \u8A00\u8A9E\u3092\u9078\u629E:" }), _jsx(Text, { dimColor: true, children: "j/k: select, Enter: confirm, Esc: cancel" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: VALID_LOCALES.map((locale, index) => {
33
+ const isSelected = index === selectedIndex;
34
+ const isCurrent = locale === currentLocale;
35
+ return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? 'cyan' : undefined, children: [isSelected ? '› ' : ' ', localeDisplayNames[locale], isCurrent ? ' (current)' : ''] }) }, locale));
36
+ }) })] }));
37
+ }
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { type ViewMode } from '../config.js';
3
3
  interface ModeSelectorProps {
4
4
  onSelect: (mode: ViewMode) => void;
5
+ onCancel?: () => void;
5
6
  }
6
- export declare function ModeSelector({ onSelect }: ModeSelectorProps): React.ReactElement;
7
+ export declare function ModeSelector({ onSelect, onCancel }: ModeSelectorProps): React.ReactElement;
7
8
  export {};
@@ -11,11 +11,15 @@ const modeDescriptions = {
11
11
  gtd: 'Classic GTD workflow with Inbox, Next, Waiting, Someday lists',
12
12
  kanban: '3-column kanban board view',
13
13
  };
14
- export function ModeSelector({ onSelect }) {
14
+ export function ModeSelector({ onSelect, onCancel }) {
15
15
  const currentMode = getViewMode();
16
16
  const initialIndex = VALID_VIEW_MODES.indexOf(currentMode);
17
17
  const [selectedIndex, setSelectedIndex] = useState(initialIndex >= 0 ? initialIndex : 0);
18
18
  useInput((input, key) => {
19
+ if (key.escape && onCancel) {
20
+ onCancel();
21
+ return;
22
+ }
19
23
  // j or down arrow: move down
20
24
  if (input === 'j' || key.downArrow) {
21
25
  setSelectedIndex((prev) => (prev < VALID_VIEW_MODES.length - 1 ? prev + 1 : 0));
@@ -29,7 +33,7 @@ export function ModeSelector({ onSelect }) {
29
33
  onSelect(VALID_VIEW_MODES[selectedIndex]);
30
34
  }
31
35
  });
32
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Select a view mode:" }), _jsx(Text, { dimColor: true, children: "j/k: select, Enter: confirm" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: VALID_VIEW_MODES.map((mode, index) => {
36
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Select a view mode:" }), _jsx(Text, { dimColor: true, children: "j/k: select, Enter: confirm, Esc: cancel" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: VALID_VIEW_MODES.map((mode, index) => {
33
37
  const isSelected = index === selectedIndex;
34
38
  const isCurrent = mode === currentMode;
35
39
  return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: isSelected ? 'cyan' : undefined, children: [isSelected ? '› ' : ' ', modeDisplayNames[mode], isCurrent ? ' (current)' : ''] }), _jsxs(Text, { dimColor: true, children: [' ', modeDescriptions[mode]] })] }, mode));
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import type { ThemeName } from './theme/types.js';
3
3
  interface ThemeSelectorProps {
4
4
  onSelect: (theme: ThemeName) => void;
5
+ onCancel?: () => void;
5
6
  }
6
- export declare function ThemeSelector({ onSelect }: ThemeSelectorProps): React.ReactElement;
7
+ export declare function ThemeSelector({ onSelect, onCancel }: ThemeSelectorProps): React.ReactElement;
7
8
  export {};
@@ -3,11 +3,15 @@ import { useState } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
4
  import { themes, VALID_THEMES } from './theme/themes.js';
5
5
  import { getThemeName } from '../config.js';
6
- export function ThemeSelector({ onSelect }) {
6
+ export function ThemeSelector({ onSelect, onCancel }) {
7
7
  const currentTheme = getThemeName();
8
8
  const initialIndex = VALID_THEMES.indexOf(currentTheme);
9
9
  const [selectedIndex, setSelectedIndex] = useState(initialIndex >= 0 ? initialIndex : 0);
10
10
  useInput((input, key) => {
11
+ if (key.escape && onCancel) {
12
+ onCancel();
13
+ return;
14
+ }
11
15
  // j or down arrow: move down
12
16
  if (input === 'j' || key.downArrow) {
13
17
  setSelectedIndex((prev) => (prev < VALID_THEMES.length - 1 ? prev + 1 : 0));
@@ -21,7 +25,7 @@ export function ThemeSelector({ onSelect }) {
21
25
  onSelect(VALID_THEMES[selectedIndex]);
22
26
  }
23
27
  });
24
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Select a theme:" }), _jsx(Text, { dimColor: true, children: "j/k: select, Enter: confirm" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: VALID_THEMES.map((themeName, index) => {
28
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Select a theme:" }), _jsx(Text, { dimColor: true, children: "j/k: select, Enter: confirm, Esc: cancel" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: VALID_THEMES.map((themeName, index) => {
25
29
  const theme = themes[themeName];
26
30
  const isSelected = index === selectedIndex;
27
31
  const isCurrent = themeName === currentTheme;
@@ -99,14 +99,14 @@ function GTDKeybindingsContent() {
99
99
  const help = i18n.tui.help;
100
100
  const theme = useTheme();
101
101
  const formatTitle = (title) => theme.style.headerUppercase ? title.toUpperCase() : title;
102
- return (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.navigation) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "1-6" }), " ", help.tabSwitch] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "h/l \u2190/\u2192" }), " ", help.prevNextTab] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "j/k \u2191/\u2193" }), " ", help.taskSelect] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.actions) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "a" }), " ", help.addTask] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "d" }), " ", help.completeTask] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "n" }), " ", help.moveToNext] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "s" }), " ", help.moveToSomeday] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "w" }), " ", help.moveToWaiting] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "i" }), " ", help.moveToInbox] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "r" }), " ", help.refresh] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.projects) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "p" }), " ", help.makeProject] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "P" }), " ", help.linkToProject] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "Enter" }), " ", help.openProject] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "Esc/b" }), " ", help.backFromProject] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.other) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "/" }), " ", help.searchTasks] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "?" }), " ", help.showHelp] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "q" }), " ", help.quit] })] })] })] }));
102
+ return (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.navigation) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "1-6" }), " ", help.tabSwitch] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "h/l \u2190/\u2192" }), " ", help.prevNextTab] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "j/k \u2191/\u2193" }), " ", help.taskSelect] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.actions) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "a" }), " ", help.addTask] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "d" }), " ", help.completeTask] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "n" }), " ", help.moveToNext] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "s" }), " ", help.moveToSomeday] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "w" }), " ", help.moveToWaiting] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "i" }), " ", help.moveToInbox] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "r" }), " ", help.refresh] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.projects) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "p" }), " ", help.makeProject] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "P" }), " ", help.linkToProject] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "Enter" }), " ", help.openProject] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "Esc/b" }), " ", help.backFromProject] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.settings) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "T" }), " ", help.changeTheme] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "V" }), " ", help.changeViewMode] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "L" }), " ", help.changeLanguage] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.other) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "/" }), " ", help.searchTasks] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "?" }), " ", help.showHelp] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "q" }), " ", help.quit] })] })] })] }));
103
103
  }
104
104
  function KanbanKeybindingsContent() {
105
105
  const i18n = t();
106
106
  const help = i18n.tui.kanbanHelp;
107
107
  const theme = useTheme();
108
108
  const formatTitle = (title) => theme.style.headerUppercase ? title.toUpperCase() : title;
109
- return (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.navigation) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "h/l \u2190/\u2192" }), " ", help.columnSwitch] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "1-3" }), " ", help.columnDirect] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "j/k \u2191/\u2193" }), " ", help.taskSelect] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.actions) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "a" }), " ", help.addTask] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "d" }), " ", help.completeTask] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "Enter" }), " ", help.moveRight] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "BS" }), " ", help.moveLeft] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.other) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "/" }), " ", help.searchTasks] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "?" }), " ", help.showHelp] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "q" }), " ", help.quit] })] })] })] }));
109
+ return (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.navigation) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "h/l \u2190/\u2192" }), " ", help.columnSwitch] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "1-3" }), " ", help.columnDirect] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "j/k \u2191/\u2193" }), " ", help.taskSelect] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.actions) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "a" }), " ", help.addTask] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "d" }), " ", help.completeTask] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "Enter" }), " ", help.moveRight] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "BS" }), " ", help.moveLeft] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.settings) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "T" }), " ", help.changeTheme] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "V" }), " ", help.changeViewMode] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "L" }), " ", help.changeLanguage] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.other) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "/" }), " ", help.searchTasks] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "?" }), " ", help.showHelp] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "q" }), " ", help.quit] })] })] })] }));
110
110
  }
111
111
  function InfoContent() {
112
112
  const i18n = t();
@@ -1,6 +1,8 @@
1
1
  import React from 'react';
2
+ type SettingsMode = 'none' | 'theme-select' | 'mode-select' | 'lang-select';
2
3
  interface KanbanBoardProps {
3
4
  onSwitchToGtd?: () => void;
5
+ onOpenSettings?: (mode: SettingsMode) => void;
4
6
  }
5
- export declare function KanbanBoard({ onSwitchToGtd }: KanbanBoardProps): React.ReactElement;
7
+ export declare function KanbanBoard({ onSwitchToGtd, onOpenSettings }: KanbanBoardProps): React.ReactElement;
6
8
  export {};
@@ -15,7 +15,7 @@ import { useTheme } from '../theme/index.js';
15
15
  import { isTursoEnabled } from '../../config.js';
16
16
  import { VERSION } from '../../version.js';
17
17
  const COLUMNS = ['todo', 'doing', 'done'];
18
- export function KanbanBoard({ onSwitchToGtd }) {
18
+ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
19
19
  const theme = useTheme();
20
20
  const { exit } = useApp();
21
21
  const [mode, setMode] = useState('normal');
@@ -140,6 +140,30 @@ export function KanbanBoard({ onSwitchToGtd }) {
140
140
  setSearchResults(results);
141
141
  setSearchResultIndex(0);
142
142
  }, [searchTasks]);
143
+ // Navigate to a task from search results
144
+ const navigateToTask = useCallback((task) => {
145
+ // Determine which column the task belongs to based on status
146
+ let targetColumn;
147
+ if (task.status === 'inbox' || task.status === 'someday') {
148
+ targetColumn = 'todo';
149
+ }
150
+ else if (task.status === 'next' || task.status === 'waiting') {
151
+ targetColumn = 'doing';
152
+ }
153
+ else {
154
+ targetColumn = 'done';
155
+ }
156
+ const columnIndex = COLUMNS.indexOf(targetColumn);
157
+ const taskIndex = tasks[targetColumn].findIndex(t => t.id === task.id);
158
+ if (columnIndex >= 0 && taskIndex >= 0) {
159
+ setCurrentColumnIndex(columnIndex);
160
+ setSelectedTaskIndices(prev => ({
161
+ ...prev,
162
+ [targetColumn]: taskIndex,
163
+ }));
164
+ setMode('normal');
165
+ }
166
+ }, [tasks]);
143
167
  const addTask = useCallback(async (title) => {
144
168
  if (!title.trim())
145
169
  return;
@@ -169,9 +193,7 @@ export function KanbanBoard({ onSwitchToGtd }) {
169
193
  if (mode === 'search') {
170
194
  if (searchResults.length > 0) {
171
195
  const task = searchResults[searchResultIndex];
172
- setSelectedTask(task);
173
- loadTaskComments(task.id);
174
- setMode('task-detail');
196
+ navigateToTask(task);
175
197
  }
176
198
  else {
177
199
  setMode('normal');
@@ -257,12 +279,12 @@ export function KanbanBoard({ onSwitchToGtd }) {
257
279
  setMode('normal');
258
280
  return;
259
281
  }
260
- // Navigate search results with Ctrl+j/k or Ctrl+n/p
261
- if (key.ctrl && (input === 'j' || input === 'n')) {
282
+ // Navigate search results with arrow keys, Ctrl+j/k, or Ctrl+n/p
283
+ if (key.downArrow || (key.ctrl && (input === 'j' || input === 'n'))) {
262
284
  setSearchResultIndex((prev) => prev < searchResults.length - 1 ? prev + 1 : 0);
263
285
  return;
264
286
  }
265
- if (key.ctrl && (input === 'k' || input === 'p')) {
287
+ if (key.upArrow || (key.ctrl && (input === 'k' || input === 'p'))) {
266
288
  setSearchResultIndex((prev) => prev > 0 ? prev - 1 : Math.max(0, searchResults.length - 1));
267
289
  return;
268
290
  }
@@ -366,6 +388,21 @@ export function KanbanBoard({ onSwitchToGtd }) {
366
388
  setSearchResultIndex(0);
367
389
  return;
368
390
  }
391
+ // Settings: Theme selector
392
+ if (input === 'T' && onOpenSettings) {
393
+ onOpenSettings('theme-select');
394
+ return;
395
+ }
396
+ // Settings: Mode selector
397
+ if (input === 'V' && onOpenSettings) {
398
+ onOpenSettings('mode-select');
399
+ return;
400
+ }
401
+ // Settings: Language selector
402
+ if (input === 'L' && onOpenSettings) {
403
+ onOpenSettings('lang-select');
404
+ return;
405
+ }
369
406
  // Quit
370
407
  if (input === 'q' || (key.ctrl && input === 'c')) {
371
408
  exit();
@@ -478,7 +515,7 @@ export function KanbanBoard({ onSwitchToGtd }) {
478
515
  const tursoEnabled = isTursoEnabled();
479
516
  return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: formatTitle(i18n.tui.title) }), _jsx(Text, { color: theme.colors.accent, children: " [KANBAN]" }), _jsx(Text, { color: theme.colors.textMuted, children: theme.name === 'modern' ? ` v${VERSION}` : ` VER ${VERSION}` }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: theme.name === 'modern'
480
517
  ? (tursoEnabled ? ' ☁️ turso' : ' 💾 local')
481
- : (tursoEnabled ? ' [DB]turso' : ' [DB]local') })] }), _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(Text, { color: theme.colors.textMuted, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` - ${selectedTask.waitingFor}`, selectedTask.dueDate && ` (${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) => {
518
+ : (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') })] }), _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(Text, { color: theme.colors.textMuted, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` - ${selectedTask.waitingFor}`, selectedTask.dueDate && ` (${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) => {
482
519
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
483
520
  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));
484
521
  })) }), 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 })) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: COLUMNS.map((column, index) => (_jsx(Box, { flexGrow: 1, flexBasis: 0, marginRight: index < 2 ? 1 : 0, children: _jsxs(Text, { color: currentColumnIndex === index ? theme.colors.textHighlight : theme.colors.textMuted, children: [index + 1, ":"] }) }, column))) }), _jsx(Box, { flexDirection: "row", children: COLUMNS.map((column, index) => (_jsx(KanbanColumn, { title: getColumnLabel(column), tasks: tasks[column], isActive: index === currentColumnIndex, selectedTaskIndex: selectedTaskIndices[column], columnIndex: index }, column))) })] })), mode === 'add' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.newTask }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.placeholder }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsx(Box, { marginTop: 1, children: mode === 'select-project' ? (_jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })) : (mode === 'task-detail' || mode === 'add-comment') ? (theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, { keys: [
@@ -12,7 +12,7 @@ export function SearchResults({ results, selectedIndex, query }) {
12
12
  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
13
  const isSelected = index === selectedIndex;
14
14
  const shortId = task.id.slice(0, 8);
15
- const statusLabel = i18n.status[task.status];
16
- 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: [" (", statusLabel, ")"] }), task.waitingFor && (_jsxs(Text, { color: theme.colors.statusWaiting, children: [" - ", task.waitingFor] }))] }) }, task.id));
15
+ const displayLabel = task.isProject ? i18n.tui.keyBar.project : i18n.status[task.status];
16
+ 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
17
  })), results.length > 10 && (_jsxs(Text, { color: theme.colors.textMuted, italic: true, children: ["... and ", results.length - 10, " more"] }))] }));
18
18
  }
@@ -15,6 +15,16 @@ export declare const solarizedLightTheme: Theme;
15
15
  export declare const synthwaveTheme: Theme;
16
16
  export declare const paperTheme: Theme;
17
17
  export declare const coffeeTheme: Theme;
18
+ export declare const nordTheme: Theme;
19
+ export declare const draculaTheme: Theme;
20
+ export declare const monokaiTheme: Theme;
21
+ export declare const gruvboxTheme: Theme;
22
+ export declare const tokyoNightTheme: Theme;
23
+ export declare const catppuccinTheme: Theme;
24
+ export declare const oceanTheme: Theme;
25
+ export declare const sakuraTheme: Theme;
26
+ export declare const msxTheme: Theme;
27
+ export declare const pc98Theme: Theme;
18
28
  export declare const themes: Record<ThemeName, Theme>;
19
29
  export declare const VALID_THEMES: ThemeName[];
20
30
  export declare function getTheme(name: ThemeName): Theme;
@@ -611,6 +611,396 @@ export const coffeeTheme = {
611
611
  loadingChars: ['☕', '○'],
612
612
  },
613
613
  };
614
+ // Nord - Arctic, north-bluish color palette
615
+ export const nordTheme = {
616
+ name: 'nord',
617
+ displayName: 'Nord',
618
+ colors: {
619
+ primary: '#88c0d0',
620
+ secondary: '#81a1c1',
621
+ accent: '#ebcb8b',
622
+ muted: '#4c566a',
623
+ border: '#4c566a',
624
+ borderActive: '#88c0d0',
625
+ background: '#2e3440',
626
+ statusInbox: '#88c0d0',
627
+ statusNext: '#a3be8c',
628
+ statusWaiting: '#ebcb8b',
629
+ statusSomeday: '#b48ead',
630
+ statusDone: '#4c566a',
631
+ text: '#eceff4',
632
+ textMuted: '#4c566a',
633
+ textSelected: '#88c0d0',
634
+ textHighlight: '#a3be8c',
635
+ fnKeyLabel: '#3b4252',
636
+ fnKeyText: '#d8dee9',
637
+ },
638
+ borders: {
639
+ main: 'round',
640
+ modal: 'round',
641
+ list: 'single',
642
+ },
643
+ style: {
644
+ selectedPrefix: '❯ ',
645
+ unselectedPrefix: ' ',
646
+ tabActiveInverse: true,
647
+ tabBrackets: ['', ''],
648
+ headerUppercase: false,
649
+ showFunctionKeys: false,
650
+ loadingChars: ['●', '○'],
651
+ },
652
+ };
653
+ // Dracula - Dark theme with vibrant colors
654
+ export const draculaTheme = {
655
+ name: 'dracula',
656
+ displayName: 'Dracula',
657
+ colors: {
658
+ primary: '#bd93f9',
659
+ secondary: '#8be9fd',
660
+ accent: '#ff79c6',
661
+ muted: '#6272a4',
662
+ border: '#6272a4',
663
+ borderActive: '#bd93f9',
664
+ background: '#282a36',
665
+ statusInbox: '#8be9fd',
666
+ statusNext: '#50fa7b',
667
+ statusWaiting: '#ffb86c',
668
+ statusSomeday: '#ff79c6',
669
+ statusDone: '#6272a4',
670
+ text: '#f8f8f2',
671
+ textMuted: '#6272a4',
672
+ textSelected: '#bd93f9',
673
+ textHighlight: '#50fa7b',
674
+ fnKeyLabel: '#44475a',
675
+ fnKeyText: '#f8f8f2',
676
+ },
677
+ borders: {
678
+ main: 'round',
679
+ modal: 'round',
680
+ list: 'single',
681
+ },
682
+ style: {
683
+ selectedPrefix: '▸ ',
684
+ unselectedPrefix: ' ',
685
+ tabActiveInverse: true,
686
+ tabBrackets: ['', ''],
687
+ headerUppercase: false,
688
+ showFunctionKeys: false,
689
+ loadingChars: ['◆', '◇'],
690
+ },
691
+ };
692
+ // Monokai - Classic editor theme with vibrant syntax colors
693
+ export const monokaiTheme = {
694
+ name: 'monokai',
695
+ displayName: 'Monokai',
696
+ colors: {
697
+ primary: '#f92672',
698
+ secondary: '#66d9ef',
699
+ accent: '#a6e22e',
700
+ muted: '#75715e',
701
+ border: '#75715e',
702
+ borderActive: '#f92672',
703
+ background: '#272822',
704
+ statusInbox: '#66d9ef',
705
+ statusNext: '#a6e22e',
706
+ statusWaiting: '#e6db74',
707
+ statusSomeday: '#ae81ff',
708
+ statusDone: '#75715e',
709
+ text: '#f8f8f2',
710
+ textMuted: '#75715e',
711
+ textSelected: '#f92672',
712
+ textHighlight: '#a6e22e',
713
+ fnKeyLabel: '#3e3d32',
714
+ fnKeyText: '#f8f8f2',
715
+ },
716
+ borders: {
717
+ main: 'single',
718
+ modal: 'round',
719
+ list: 'single',
720
+ },
721
+ style: {
722
+ selectedPrefix: '» ',
723
+ unselectedPrefix: ' ',
724
+ tabActiveInverse: true,
725
+ tabBrackets: ['', ''],
726
+ headerUppercase: false,
727
+ showFunctionKeys: false,
728
+ loadingChars: ['▓', '░'],
729
+ },
730
+ };
731
+ // Gruvbox - Retro groove color scheme
732
+ export const gruvboxTheme = {
733
+ name: 'gruvbox',
734
+ displayName: 'Gruvbox',
735
+ colors: {
736
+ primary: '#fe8019',
737
+ secondary: '#fabd2f',
738
+ accent: '#b8bb26',
739
+ muted: '#665c54',
740
+ border: '#665c54',
741
+ borderActive: '#fe8019',
742
+ background: '#282828',
743
+ statusInbox: '#83a598',
744
+ statusNext: '#b8bb26',
745
+ statusWaiting: '#fabd2f',
746
+ statusSomeday: '#d3869b',
747
+ statusDone: '#665c54',
748
+ text: '#ebdbb2',
749
+ textMuted: '#928374',
750
+ textSelected: '#fe8019',
751
+ textHighlight: '#b8bb26',
752
+ fnKeyLabel: '#3c3836',
753
+ fnKeyText: '#ebdbb2',
754
+ },
755
+ borders: {
756
+ main: 'single',
757
+ modal: 'single',
758
+ list: 'single',
759
+ },
760
+ style: {
761
+ selectedPrefix: '▶ ',
762
+ unselectedPrefix: ' ',
763
+ tabActiveInverse: true,
764
+ tabBrackets: ['[', ']'],
765
+ headerUppercase: false,
766
+ showFunctionKeys: false,
767
+ loadingChars: ['█', '░'],
768
+ },
769
+ };
770
+ // Tokyo Night - A clean dark theme inspired by Tokyo's night lights
771
+ export const tokyoNightTheme = {
772
+ name: 'tokyo-night',
773
+ displayName: 'Tokyo Night',
774
+ colors: {
775
+ primary: '#7aa2f7',
776
+ secondary: '#bb9af7',
777
+ accent: '#7dcfff',
778
+ muted: '#565f89',
779
+ border: '#565f89',
780
+ borderActive: '#7aa2f7',
781
+ background: '#1a1b26',
782
+ statusInbox: '#7aa2f7',
783
+ statusNext: '#9ece6a',
784
+ statusWaiting: '#e0af68',
785
+ statusSomeday: '#bb9af7',
786
+ statusDone: '#565f89',
787
+ text: '#c0caf5',
788
+ textMuted: '#565f89',
789
+ textSelected: '#7dcfff',
790
+ textHighlight: '#9ece6a',
791
+ fnKeyLabel: '#24283b',
792
+ fnKeyText: '#c0caf5',
793
+ },
794
+ borders: {
795
+ main: 'round',
796
+ modal: 'round',
797
+ list: 'single',
798
+ },
799
+ style: {
800
+ selectedPrefix: '› ',
801
+ unselectedPrefix: ' ',
802
+ tabActiveInverse: true,
803
+ tabBrackets: ['', ''],
804
+ headerUppercase: false,
805
+ showFunctionKeys: false,
806
+ loadingChars: ['◉', '◎'],
807
+ },
808
+ };
809
+ // Catppuccin - Soothing pastel theme (Mocha variant)
810
+ export const catppuccinTheme = {
811
+ name: 'catppuccin',
812
+ displayName: 'Catppuccin',
813
+ colors: {
814
+ primary: '#cba6f7',
815
+ secondary: '#89b4fa',
816
+ accent: '#f5c2e7',
817
+ muted: '#6c7086',
818
+ border: '#6c7086',
819
+ borderActive: '#cba6f7',
820
+ background: '#1e1e2e',
821
+ statusInbox: '#89b4fa',
822
+ statusNext: '#a6e3a1',
823
+ statusWaiting: '#f9e2af',
824
+ statusSomeday: '#f5c2e7',
825
+ statusDone: '#6c7086',
826
+ text: '#cdd6f4',
827
+ textMuted: '#6c7086',
828
+ textSelected: '#cba6f7',
829
+ textHighlight: '#a6e3a1',
830
+ fnKeyLabel: '#313244',
831
+ fnKeyText: '#cdd6f4',
832
+ },
833
+ borders: {
834
+ main: 'round',
835
+ modal: 'round',
836
+ list: 'single',
837
+ },
838
+ style: {
839
+ selectedPrefix: '◆ ',
840
+ unselectedPrefix: ' ',
841
+ tabActiveInverse: true,
842
+ tabBrackets: ['', ''],
843
+ headerUppercase: false,
844
+ showFunctionKeys: false,
845
+ loadingChars: ['●', '○'],
846
+ },
847
+ };
848
+ // Ocean - Deep sea inspired blue theme
849
+ export const oceanTheme = {
850
+ name: 'ocean',
851
+ displayName: 'Ocean',
852
+ colors: {
853
+ primary: '#00b4d8',
854
+ secondary: '#0077b6',
855
+ accent: '#90e0ef',
856
+ muted: '#023e8a',
857
+ border: '#0077b6',
858
+ borderActive: '#00b4d8',
859
+ background: '#03045e',
860
+ statusInbox: '#00b4d8',
861
+ statusNext: '#48cae4',
862
+ statusWaiting: '#90e0ef',
863
+ statusSomeday: '#ade8f4',
864
+ statusDone: '#023e8a',
865
+ text: '#caf0f8',
866
+ textMuted: '#0077b6',
867
+ textSelected: '#90e0ef',
868
+ textHighlight: '#48cae4',
869
+ fnKeyLabel: '#023e8a',
870
+ fnKeyText: '#caf0f8',
871
+ },
872
+ borders: {
873
+ main: 'round',
874
+ modal: 'round',
875
+ list: 'single',
876
+ },
877
+ style: {
878
+ selectedPrefix: '≋ ',
879
+ unselectedPrefix: ' ',
880
+ tabActiveInverse: true,
881
+ tabBrackets: ['〔', '〕'],
882
+ headerUppercase: false,
883
+ showFunctionKeys: false,
884
+ loadingChars: ['◐', '◑'],
885
+ },
886
+ };
887
+ // Sakura - Cherry blossom inspired pink theme
888
+ export const sakuraTheme = {
889
+ name: 'sakura',
890
+ displayName: 'Sakura',
891
+ colors: {
892
+ primary: '#ffb7c5',
893
+ secondary: '#ff8fa3',
894
+ accent: '#ffffff',
895
+ muted: '#c9184a',
896
+ border: '#ff8fa3',
897
+ borderActive: '#ffb7c5',
898
+ background: '#590d22',
899
+ statusInbox: '#ffb7c5',
900
+ statusNext: '#ff758f',
901
+ statusWaiting: '#ffccd5',
902
+ statusSomeday: '#ff4d6d',
903
+ statusDone: '#800f2f',
904
+ text: '#fff0f3',
905
+ textMuted: '#a4133c',
906
+ textSelected: '#ffffff',
907
+ textHighlight: '#ff758f',
908
+ fnKeyLabel: '#800f2f',
909
+ fnKeyText: '#ffb7c5',
910
+ },
911
+ borders: {
912
+ main: 'round',
913
+ modal: 'round',
914
+ list: 'single',
915
+ },
916
+ style: {
917
+ selectedPrefix: '❀ ',
918
+ unselectedPrefix: ' ',
919
+ tabActiveInverse: true,
920
+ tabBrackets: ['『', '』'],
921
+ headerUppercase: false,
922
+ showFunctionKeys: false,
923
+ loadingChars: ['✿', '❀'],
924
+ },
925
+ };
926
+ // MSX - Japanese retro computer style (TMS9918 palette)
927
+ export const msxTheme = {
928
+ name: 'msx',
929
+ displayName: 'MSX',
930
+ colors: {
931
+ primary: '#40b64a',
932
+ secondary: '#5955df',
933
+ accent: '#ffffff',
934
+ muted: '#cacaca',
935
+ border: '#40b64a',
936
+ borderActive: '#73ce7c',
937
+ background: '#000000',
938
+ statusInbox: '#64daee',
939
+ statusNext: '#40b64a',
940
+ statusWaiting: '#ddce85',
941
+ statusSomeday: '#b565b3',
942
+ statusDone: '#cacaca',
943
+ text: '#40b64a',
944
+ textMuted: '#cacaca',
945
+ textSelected: '#ffffff',
946
+ textHighlight: '#73ce7c',
947
+ fnKeyLabel: '#64daee',
948
+ fnKeyText: '#000000',
949
+ },
950
+ borders: {
951
+ main: 'single',
952
+ modal: 'single',
953
+ list: 'single',
954
+ },
955
+ style: {
956
+ selectedPrefix: '> ',
957
+ unselectedPrefix: ' ',
958
+ tabActiveInverse: true,
959
+ tabBrackets: ['[', ']'],
960
+ headerUppercase: true,
961
+ showFunctionKeys: true,
962
+ loadingChars: ['■', '□'],
963
+ },
964
+ };
965
+ // PC-98 - NEC PC-9801 style
966
+ export const pc98Theme = {
967
+ name: 'pc-98',
968
+ displayName: 'PC-98',
969
+ colors: {
970
+ primary: '#ffffff',
971
+ secondary: '#00ffff',
972
+ accent: '#ffff00',
973
+ muted: '#808080',
974
+ border: '#ffffff',
975
+ borderActive: '#00ffff',
976
+ background: '#000080',
977
+ statusInbox: '#00ffff',
978
+ statusNext: '#00ff00',
979
+ statusWaiting: '#ffff00',
980
+ statusSomeday: '#ff00ff',
981
+ statusDone: '#808080',
982
+ text: '#ffffff',
983
+ textMuted: '#808080',
984
+ textSelected: '#ffff00',
985
+ textHighlight: '#00ffff',
986
+ fnKeyLabel: '#00ffff',
987
+ fnKeyText: '#ffffff',
988
+ },
989
+ borders: {
990
+ main: 'single',
991
+ modal: 'double',
992
+ list: 'single',
993
+ },
994
+ style: {
995
+ selectedPrefix: '▶ ',
996
+ unselectedPrefix: ' ',
997
+ tabActiveInverse: true,
998
+ tabBrackets: ['【', '】'],
999
+ headerUppercase: false,
1000
+ showFunctionKeys: true,
1001
+ loadingChars: ['●', '○'],
1002
+ },
1003
+ };
614
1004
  export const themes = {
615
1005
  'modern': modernTheme,
616
1006
  'norton-commander': nortonCommanderTheme,
@@ -628,6 +1018,16 @@ export const themes = {
628
1018
  'synthwave': synthwaveTheme,
629
1019
  'paper': paperTheme,
630
1020
  'coffee': coffeeTheme,
1021
+ 'nord': nordTheme,
1022
+ 'dracula': draculaTheme,
1023
+ 'monokai': monokaiTheme,
1024
+ 'gruvbox': gruvboxTheme,
1025
+ 'tokyo-night': tokyoNightTheme,
1026
+ 'catppuccin': catppuccinTheme,
1027
+ 'ocean': oceanTheme,
1028
+ 'sakura': sakuraTheme,
1029
+ 'msx': msxTheme,
1030
+ 'pc-98': pc98Theme,
631
1031
  };
632
1032
  export const VALID_THEMES = [
633
1033
  'modern',
@@ -646,6 +1046,16 @@ export const VALID_THEMES = [
646
1046
  'synthwave',
647
1047
  'paper',
648
1048
  'coffee',
1049
+ 'nord',
1050
+ 'dracula',
1051
+ 'monokai',
1052
+ 'gruvbox',
1053
+ 'tokyo-night',
1054
+ 'catppuccin',
1055
+ 'ocean',
1056
+ 'sakura',
1057
+ 'msx',
1058
+ 'pc-98',
649
1059
  ];
650
1060
  export function getTheme(name) {
651
1061
  return themes[name] || modernTheme;
@@ -1,4 +1,4 @@
1
- export type ThemeName = 'modern' | 'norton-commander' | 'dos-prompt' | 'turbo-pascal' | 'classic-mac' | 'apple-ii' | 'commodore-64' | 'amiga-workbench' | 'matrix' | 'amber-crt' | 'phosphor' | 'solarized-dark' | 'solarized-light' | 'synthwave' | 'paper' | 'coffee';
1
+ export type ThemeName = 'modern' | 'norton-commander' | 'dos-prompt' | 'turbo-pascal' | 'classic-mac' | 'apple-ii' | 'commodore-64' | 'amiga-workbench' | 'matrix' | 'amber-crt' | 'phosphor' | 'solarized-dark' | 'solarized-light' | 'synthwave' | 'paper' | 'coffee' | 'nord' | 'dracula' | 'monokai' | 'gruvbox' | 'tokyo-night' | 'catppuccin' | 'ocean' | 'sakura' | 'msx' | 'pc-98';
2
2
  export type BorderStyleType = 'single' | 'double' | 'round' | 'bold' | 'singleDouble' | 'doubleSingle' | 'classic';
3
3
  export interface ThemeColors {
4
4
  primary: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "floq",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Floq - Getting Things Done Task Manager with MS-DOS style themes",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",