floq 0.2.0 → 0.2.2

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
@@ -228,7 +228,7 @@ floq config turso --disable
228
228
 
229
229
  ## テーマ
230
230
 
231
- 16種類のテーマが利用可能。`floq config theme` でインタラクティブに選択(j/kで移動)。
231
+ 26種類のテーマが利用可能。`floq config theme` でインタラクティブに選択(j/kで移動)。
232
232
 
233
233
  | テーマ | 説明 |
234
234
  |--------|------|
@@ -248,6 +248,16 @@ floq config turso --disable
248
248
  | `synthwave` | ネオン80sスタイル |
249
249
  | `paper` | 紙とインク風ライト |
250
250
  | `coffee` | 暖かみのある茶系 |
251
+ | `nord` | 北欧風ブルーグレー |
252
+ | `dracula` | ダーク&ビビッドカラー |
253
+ | `monokai` | エディタ定番の鮮やかな配色 |
254
+ | `gruvbox` | レトロな暖色系ダーク |
255
+ | `tokyo-night` | 東京の夜景イメージ |
256
+ | `catppuccin` | パステル系モダン |
257
+ | `ocean` | 深海ブルー |
258
+ | `sakura` | 桜ピンク |
259
+ | `msx` | MSXコンピュータ(TMS9918) |
260
+ | `pc-98` | NEC PC-9801風 |
251
261
 
252
262
  > **注意**: 背景色はターミナルの設定に依存します。
253
263
 
package/README.md CHANGED
@@ -228,7 +228,7 @@ floq config turso --disable
228
228
 
229
229
  ## Themes
230
230
 
231
- 16 themes available. Use `floq config theme` for interactive selection (j/k to navigate).
231
+ 26 themes available. Use `floq config theme` for interactive selection (j/k to navigate).
232
232
 
233
233
  | Theme | Description |
234
234
  |-------|-------------|
@@ -248,6 +248,16 @@ floq config turso --disable
248
248
  | `synthwave` | Neon 80s aesthetic |
249
249
  | `paper` | Light minimal theme |
250
250
  | `coffee` | Warm brown tones |
251
+ | `nord` | Arctic, north-bluish palette |
252
+ | `dracula` | Dark theme with vibrant colors |
253
+ | `monokai` | Classic editor vivid colors |
254
+ | `gruvbox` | Retro groove warm tones |
255
+ | `tokyo-night` | Tokyo night lights inspired |
256
+ | `catppuccin` | Soothing pastel theme |
257
+ | `ocean` | Deep sea blue theme |
258
+ | `sakura` | Cherry blossom pink |
259
+ | `msx` | MSX computer (TMS9918) |
260
+ | `pc-98` | NEC PC-9801 style |
251
261
 
252
262
  > **Note**: Background colors depend on your terminal settings.
253
263
 
package/dist/i18n/en.d.ts CHANGED
@@ -107,6 +107,7 @@ export declare const en: {
107
107
  title: string;
108
108
  whatsNewTab: string;
109
109
  keybindingsTab: string;
110
+ infoTab: string;
110
111
  tabHint: string;
111
112
  navigation: string;
112
113
  tabSwitch: string;
@@ -128,6 +129,10 @@ export declare const en: {
128
129
  taskDetail: string;
129
130
  addComment: string;
130
131
  searchTasks: string;
132
+ settings: string;
133
+ changeTheme: string;
134
+ changeViewMode: string;
135
+ changeLanguage: string;
131
136
  other: string;
132
137
  showHelp: string;
133
138
  quit: string;
@@ -155,6 +160,10 @@ export declare const en: {
155
160
  moveRight: string;
156
161
  moveLeft: string;
157
162
  searchTasks: string;
163
+ settings: string;
164
+ changeTheme: string;
165
+ changeViewMode: string;
166
+ changeLanguage: string;
158
167
  other: string;
159
168
  showHelp: string;
160
169
  quit: string;
@@ -168,6 +177,10 @@ export declare const en: {
168
177
  commentDeleted: string;
169
178
  taskDetailTitle: string;
170
179
  taskDetailFooter: string;
180
+ taskDetailStatus: string;
181
+ deleteConfirm: string;
182
+ deleted: string;
183
+ deleteCancelled: string;
171
184
  comments: string;
172
185
  projectTasks: string;
173
186
  search: {
@@ -178,6 +191,21 @@ export declare const en: {
178
191
  resultsTitle: string;
179
192
  searchTasks: string;
180
193
  };
194
+ info: {
195
+ settings: string;
196
+ database: string;
197
+ paths: string;
198
+ theme: string;
199
+ language: string;
200
+ viewMode: string;
201
+ dbType: string;
202
+ dbPath: string;
203
+ tursoUrl: string;
204
+ configFile: string;
205
+ dataDir: string;
206
+ local: string;
207
+ turso: string;
208
+ };
181
209
  };
182
210
  setup: {
183
211
  welcome: {
@@ -232,6 +260,7 @@ export type HelpTranslations = {
232
260
  title: string;
233
261
  whatsNewTab: string;
234
262
  keybindingsTab: string;
263
+ infoTab: string;
235
264
  tabHint: string;
236
265
  navigation: string;
237
266
  tabSwitch: string;
@@ -253,6 +282,10 @@ export type HelpTranslations = {
253
282
  taskDetail: string;
254
283
  addComment: string;
255
284
  searchTasks: string;
285
+ settings: string;
286
+ changeTheme: string;
287
+ changeViewMode: string;
288
+ changeLanguage: string;
256
289
  other: string;
257
290
  showHelp: string;
258
291
  quit: string;
@@ -280,6 +313,10 @@ export type KanbanHelpTranslations = {
280
313
  moveRight: string;
281
314
  moveLeft: string;
282
315
  searchTasks: string;
316
+ settings: string;
317
+ changeTheme: string;
318
+ changeViewMode: string;
319
+ changeLanguage: string;
283
320
  other: string;
284
321
  showHelp: string;
285
322
  quit: string;
@@ -312,6 +349,21 @@ export type SearchTranslations = {
312
349
  resultsTitle: string;
313
350
  searchTasks: string;
314
351
  };
352
+ export type InfoTranslations = {
353
+ settings: string;
354
+ database: string;
355
+ paths: string;
356
+ theme: string;
357
+ language: string;
358
+ viewMode: string;
359
+ dbType: string;
360
+ dbPath: string;
361
+ tursoUrl: string;
362
+ configFile: string;
363
+ dataDir: string;
364
+ local: string;
365
+ turso: string;
366
+ };
315
367
  export type TuiTranslations = {
316
368
  title: string;
317
369
  helpHint: string;
@@ -342,6 +394,7 @@ export type TuiTranslations = {
342
394
  whatsNew: WhatsNewTranslations;
343
395
  kanbanHelp: KanbanHelpTranslations;
344
396
  search: SearchTranslations;
397
+ info: InfoTranslations;
345
398
  addComment: string;
346
399
  noComments: string;
347
400
  commentHint: string;
@@ -350,6 +403,10 @@ export type TuiTranslations = {
350
403
  commentDeleted: string;
351
404
  taskDetailTitle: string;
352
405
  taskDetailFooter: string;
406
+ taskDetailStatus: string;
407
+ deleteConfirm: string;
408
+ deleted: string;
409
+ deleteCancelled: string;
353
410
  comments: string;
354
411
  projectTasks: string;
355
412
  };
package/dist/i18n/en.js CHANGED
@@ -81,7 +81,7 @@ export const en = {
81
81
  movedToWaiting: 'Moved "{title}" to Waiting (for {person})',
82
82
  waitingFor: 'Waiting for: ',
83
83
  refreshed: 'Refreshed',
84
- footer: 'a=add d=done n=next s=someday w=waiting i=inbox p=project P=link',
84
+ footer: 'a=add d=done D=delete n=next s=someday w=waiting i=inbox p=project P=link',
85
85
  noTasks: 'No tasks',
86
86
  // Tab labels
87
87
  tabInbox: 'Inbox',
@@ -116,6 +116,7 @@ export const en = {
116
116
  title: 'Keyboard Shortcuts',
117
117
  whatsNewTab: "What's New",
118
118
  keybindingsTab: 'Keybindings',
119
+ infoTab: 'Info',
119
120
  tabHint: 'Tab: switch view',
120
121
  navigation: 'Navigation',
121
122
  tabSwitch: 'Switch tab (5=Projects, 6=Done)',
@@ -137,6 +138,10 @@ export const en = {
137
138
  taskDetail: 'View task details',
138
139
  addComment: 'Add comment',
139
140
  searchTasks: 'Search tasks',
141
+ settings: 'Settings',
142
+ changeTheme: 'Change theme',
143
+ changeViewMode: 'Change view mode',
144
+ changeLanguage: 'Change language',
140
145
  other: 'Other',
141
146
  showHelp: 'Show this help',
142
147
  quit: 'Quit',
@@ -166,6 +171,10 @@ export const en = {
166
171
  moveRight: 'Move task to next column',
167
172
  moveLeft: 'Move task to previous column',
168
173
  searchTasks: 'Search tasks',
174
+ settings: 'Settings',
175
+ changeTheme: 'Change theme',
176
+ changeViewMode: 'Change view mode',
177
+ changeLanguage: 'Change language',
169
178
  other: 'Other',
170
179
  showHelp: 'Show this help',
171
180
  quit: 'Quit',
@@ -179,6 +188,10 @@ export const en = {
179
188
  commentDeleted: 'Comment deleted',
180
189
  taskDetailTitle: 'Task Details',
181
190
  taskDetailFooter: 'j/k=select i=comment d=delete P=link b/Esc=back',
191
+ taskDetailStatus: 'Status',
192
+ deleteConfirm: 'Delete "{title}"? (y/n)',
193
+ deleted: 'Deleted: "{title}"',
194
+ deleteCancelled: 'Delete cancelled',
182
195
  comments: 'Comments',
183
196
  projectTasks: 'Tasks',
184
197
  // Search
@@ -190,6 +203,22 @@ export const en = {
190
203
  resultsTitle: 'Search Results',
191
204
  searchTasks: 'Search tasks',
192
205
  },
206
+ // Info tab
207
+ info: {
208
+ settings: 'Settings',
209
+ database: 'Database',
210
+ paths: 'Paths',
211
+ theme: 'Theme',
212
+ language: 'Language',
213
+ viewMode: 'View Mode',
214
+ dbType: 'Type',
215
+ dbPath: 'Path',
216
+ tursoUrl: 'Turso URL',
217
+ configFile: 'Config File',
218
+ dataDir: 'Data Directory',
219
+ local: 'local',
220
+ turso: 'turso',
221
+ },
193
222
  },
194
223
  // Setup wizard
195
224
  setup: {
package/dist/i18n/ja.js CHANGED
@@ -81,7 +81,7 @@ export const ja = {
81
81
  movedToWaiting: '「{title}」を連絡待ち({person})に移動しました',
82
82
  waitingFor: '待機相手: ',
83
83
  refreshed: '更新しました',
84
- footer: 'a=追加 d=完了 n=次 s=いつか w=待ち i=Inbox p=プロジェクト化 P=紐づけ',
84
+ footer: 'a=追加 d=完了 D=削除 n=次 s=いつか w=待ち i=Inbox p=プロジェクト化 P=紐づけ',
85
85
  noTasks: 'タスクなし',
86
86
  // Tab labels
87
87
  tabInbox: 'Inbox',
@@ -116,6 +116,7 @@ export const ja = {
116
116
  title: 'キーボードショートカット',
117
117
  whatsNewTab: '更新履歴',
118
118
  keybindingsTab: 'キー操作',
119
+ infoTab: '情報',
119
120
  tabHint: 'Tab: 表示切替',
120
121
  navigation: 'ナビゲーション',
121
122
  tabSwitch: 'タブ切替 (5=プロジェクト, 6=完了)',
@@ -137,6 +138,10 @@ export const ja = {
137
138
  taskDetail: 'タスク詳細を表示',
138
139
  addComment: 'コメント追加',
139
140
  searchTasks: 'タスク検索',
141
+ settings: '設定',
142
+ changeTheme: 'テーマ変更',
143
+ changeViewMode: '表示モード変更',
144
+ changeLanguage: '言語変更',
140
145
  other: 'その他',
141
146
  showHelp: 'このヘルプを表示',
142
147
  quit: '終了',
@@ -166,6 +171,10 @@ export const ja = {
166
171
  moveRight: '次のカラムへ移動',
167
172
  moveLeft: '前のカラムへ戻す',
168
173
  searchTasks: 'タスク検索',
174
+ settings: '設定',
175
+ changeTheme: 'テーマ変更',
176
+ changeViewMode: '表示モード変更',
177
+ changeLanguage: '言語変更',
169
178
  other: 'その他',
170
179
  showHelp: 'このヘルプを表示',
171
180
  quit: '終了',
@@ -179,6 +188,10 @@ export const ja = {
179
188
  commentDeleted: 'コメントを削除しました',
180
189
  taskDetailTitle: 'タスク詳細',
181
190
  taskDetailFooter: 'j/k=選択 i=コメント d=削除 P=紐づけ b/Esc=戻る',
191
+ taskDetailStatus: 'ステータス',
192
+ deleteConfirm: '「{title}」を削除しますか? (y/n)',
193
+ deleted: '削除しました: 「{title}」',
194
+ deleteCancelled: '削除をキャンセルしました',
182
195
  comments: 'コメント',
183
196
  projectTasks: 'タスク一覧',
184
197
  // Search
@@ -190,6 +203,22 @@ export const ja = {
190
203
  resultsTitle: '検索結果',
191
204
  searchTasks: 'タスク検索',
192
205
  },
206
+ // Info tab
207
+ info: {
208
+ settings: '設定',
209
+ database: 'データベース',
210
+ paths: 'パス',
211
+ theme: 'テーマ',
212
+ language: '言語',
213
+ viewMode: '表示モード',
214
+ dbType: '種類',
215
+ dbPath: 'パス',
216
+ tursoUrl: 'Turso URL',
217
+ configFile: '設定ファイル',
218
+ dataDir: 'データディレクトリ',
219
+ local: 'ローカル',
220
+ turso: 'turso',
221
+ },
193
222
  },
194
223
  // Setup wizard
195
224
  setup: {
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, getTursoConfig } 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');
@@ -46,6 +79,7 @@ function AppContent() {
46
79
  const [taskComments, setTaskComments] = useState([]);
47
80
  const [selectedCommentIndex, setSelectedCommentIndex] = useState(0);
48
81
  const [taskToWaiting, setTaskToWaiting] = useState(null);
82
+ const [taskToDelete, setTaskToDelete] = useState(null);
49
83
  const [projectProgress, setProjectProgress] = useState({});
50
84
  // Search state
51
85
  const [searchQuery, setSearchQuery] = useState('');
@@ -267,6 +301,15 @@ function AppContent() {
267
301
  setMessage(fmt(i18n.tui.madeProject || 'Made project: {title}', { title: task.title }));
268
302
  await loadTasks();
269
303
  }, [i18n.tui.madeProject, loadTasks]);
304
+ const deleteTask = useCallback(async (task) => {
305
+ const db = getDb();
306
+ // Delete comments first
307
+ await db.delete(schema.comments).where(eq(schema.comments.taskId, task.id));
308
+ // Delete the task
309
+ await db.delete(schema.tasks).where(eq(schema.tasks.id, task.id));
310
+ setMessage(fmt(i18n.tui.deleted || 'Deleted: "{title}"', { title: task.title }));
311
+ await loadTasks();
312
+ }, [i18n.tui.deleted, loadTasks]);
270
313
  const getTabLabel = (tab) => {
271
314
  switch (tab) {
272
315
  case 'inbox':
@@ -314,6 +357,27 @@ function AppContent() {
314
357
  // Let TextInput handle other keys
315
358
  return;
316
359
  }
360
+ // Handle confirm-delete mode
361
+ if (mode === 'confirm-delete' && taskToDelete) {
362
+ if (input === 'y' || input === 'Y') {
363
+ deleteTask(taskToDelete).then(() => {
364
+ if (selectedTaskIndex >= currentTasks.length - 1) {
365
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
366
+ }
367
+ });
368
+ setTaskToDelete(null);
369
+ setMode('normal');
370
+ return;
371
+ }
372
+ if (input === 'n' || input === 'N' || key.escape) {
373
+ setMessage(i18n.tui.deleteCancelled || 'Delete cancelled');
374
+ setTaskToDelete(null);
375
+ setMode('normal');
376
+ return;
377
+ }
378
+ // Ignore other keys in confirm mode
379
+ return;
380
+ }
317
381
  // Handle add mode
318
382
  if (mode === 'add' || mode === 'add-to-project' || mode === 'add-comment' || mode === 'move-to-waiting') {
319
383
  if (key.escape) {
@@ -520,6 +584,21 @@ function AppContent() {
520
584
  setSearchResultIndex(0);
521
585
  return;
522
586
  }
587
+ // Settings: Theme selector
588
+ if (input === 'T') {
589
+ onOpenSettings('theme-select');
590
+ return;
591
+ }
592
+ // Settings: Mode selector
593
+ if (input === 'V') {
594
+ onOpenSettings('mode-select');
595
+ return;
596
+ }
597
+ // Settings: Language selector
598
+ if (input === 'L') {
599
+ onOpenSettings('lang-select');
600
+ return;
601
+ }
523
602
  // Quit
524
603
  if (input === 'q' || (key.ctrl && input === 'c')) {
525
604
  exit();
@@ -626,6 +705,13 @@ function AppContent() {
626
705
  });
627
706
  return;
628
707
  }
708
+ // Delete task (D key - with confirmation)
709
+ if (input === 'D' && currentTasks.length > 0 && currentTab !== 'projects') {
710
+ const task = currentTasks[selectedTaskIndex];
711
+ setTaskToDelete(task);
712
+ setMode('confirm-delete');
713
+ return;
714
+ }
629
715
  // Move to next actions
630
716
  if (input === 'n' && currentTasks.length > 0 && currentTab !== 'next' && currentTab !== 'projects' && currentTab !== 'done') {
631
717
  const task = currentTasks[selectedTaskIndex];
@@ -685,24 +771,14 @@ function AppContent() {
685
771
  };
686
772
  // Turso 接続情報を取得
687
773
  const tursoEnabled = isTursoEnabled();
688
- const tursoHost = tursoEnabled ? (() => {
689
- const config = getTursoConfig();
690
- if (config) {
691
- try {
692
- return new URL(config.url).host;
693
- }
694
- catch {
695
- return config.url;
696
- }
697
- }
698
- return '';
699
- })() : '';
700
- 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}` }), tursoEnabled && (_jsxs(Text, { color: theme.colors.accent, children: [theme.name === 'modern' ? ' ☁️ ' : ' [SYNC] ', tursoHost] })), !tursoEnabled && (_jsx(Text, { color: theme.colors.textMuted, children: theme.name === 'modern' ? ' 💾 local' : ' [LOCAL]' }))] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
774
+ 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'
775
+ ? (tursoEnabled ? ' ☁️ turso' : ' 💾 local')
776
+ : (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') })] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
701
777
  const isActive = index === currentListIndex && mode !== 'project-detail';
702
778
  const count = tasks[tab].length;
703
779
  const label = `${index + 1}:${getTabLabel(tab)}(${count})`;
704
780
  return (_jsx(Box, { marginRight: 1, children: _jsx(Text, { color: isActive ? theme.colors.textSelected : theme.colors.textMuted, bold: isActive, inverse: isActive && theme.style.tabActiveInverse, children: formatTabLabel(` ${label} `, isActive) }) }, tab));
705
- }) }), mode === 'project-detail' && selectedProject && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📁 ' : '>> ', selectedProject.title] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ")"] })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.projectTasks || 'Tasks', " (", projectTasks.length, ")"] }) })] })), (mode === 'task-detail' || mode === 'add-comment') && 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}`] })] }), _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) => {
781
+ }) }), mode === 'project-detail' && selectedProject && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📁 ' : '>> ', selectedProject.title] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ")"] })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.projectTasks || 'Tasks', " (", projectTasks.length, ")"] }) })] })), (mode === 'task-detail' || mode === 'add-comment') && selectedTask && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📋 ' : '>> ', i18n.tui.taskDetailTitle || 'Task Details'] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ", ", i18n.tui.commentHint || 'i: add comment', ")"] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus, ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] })] }), _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) => {
706
782
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
707
783
  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));
708
784
  })) }), 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 === 'search' && searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery })), mode !== 'task-detail' && mode !== 'add-comment' && mode !== 'search' && (_jsx(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, minHeight: 10, children: currentTasks.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noTasks })) : (currentTasks.map((task, index) => {
@@ -711,7 +787,7 @@ function AppContent() {
711
787
  return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress }, task.id));
712
788
  })) })), (mode === 'add' || mode === 'add-to-project') && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: mode === 'add-to-project' && selectedProject
713
789
  ? `${i18n.tui.newTask}[${selectedProject.title}] `
714
- : 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 === 'move-to-waiting' && taskToWaiting && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.waitingFor }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'select-project' && taskToLink && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: tasks.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(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 === 'task-detail' || mode === 'add-comment') ? (theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, { keys: [
790
+ : 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 === 'move-to-waiting' && taskToWaiting && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.waitingFor }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'select-project' && taskToLink && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: tasks.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(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), mode === 'confirm-delete' && taskToDelete && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsx(Box, { marginTop: 1, children: (mode === 'task-detail' || mode === 'add-comment') ? (theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, { keys: [
715
791
  { key: 'i', label: i18n.tui.keyBar.comment },
716
792
  { key: 'd', label: i18n.tui.keyBar.delete },
717
793
  { key: 'P', label: i18n.tui.keyBar.project },
@@ -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;