floq 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.ja.md +157 -29
  2. package/README.md +157 -29
  3. package/dist/changelog.d.ts +13 -0
  4. package/dist/changelog.js +95 -0
  5. package/dist/cli.js +70 -1
  6. package/dist/commands/add.js +5 -6
  7. package/dist/commands/comment.d.ts +2 -0
  8. package/dist/commands/comment.js +67 -0
  9. package/dist/commands/config.d.ts +7 -0
  10. package/dist/commands/config.js +163 -14
  11. package/dist/commands/done.js +4 -6
  12. package/dist/commands/list.js +9 -13
  13. package/dist/commands/move.js +4 -6
  14. package/dist/commands/project.js +18 -26
  15. package/dist/commands/setup.d.ts +1 -0
  16. package/dist/commands/setup.js +13 -0
  17. package/dist/config.d.ts +15 -1
  18. package/dist/config.js +53 -2
  19. package/dist/db/index.d.ts +5 -4
  20. package/dist/db/index.js +127 -32
  21. package/dist/db/schema.d.ts +83 -0
  22. package/dist/db/schema.js +6 -0
  23. package/dist/i18n/en.d.ts +258 -0
  24. package/dist/i18n/en.js +138 -3
  25. package/dist/i18n/ja.js +138 -3
  26. package/dist/index.js +33 -1
  27. package/dist/paths.d.ts +4 -0
  28. package/dist/paths.js +63 -5
  29. package/dist/ui/App.js +384 -136
  30. package/dist/ui/ModeSelector.d.ts +7 -0
  31. package/dist/ui/ModeSelector.js +37 -0
  32. package/dist/ui/SetupWizard.d.ts +6 -0
  33. package/dist/ui/SetupWizard.js +321 -0
  34. package/dist/ui/ThemeSelector.d.ts +1 -1
  35. package/dist/ui/ThemeSelector.js +23 -10
  36. package/dist/ui/components/FunctionKeyBar.d.ts +5 -4
  37. package/dist/ui/components/FunctionKeyBar.js +19 -15
  38. package/dist/ui/components/HelpModal.d.ts +2 -1
  39. package/dist/ui/components/HelpModal.js +118 -4
  40. package/dist/ui/components/KanbanBoard.d.ts +6 -0
  41. package/dist/ui/components/KanbanBoard.js +508 -0
  42. package/dist/ui/components/KanbanColumn.d.ts +12 -0
  43. package/dist/ui/components/KanbanColumn.js +11 -0
  44. package/dist/ui/components/ProgressBar.d.ts +7 -0
  45. package/dist/ui/components/ProgressBar.js +13 -0
  46. package/dist/ui/components/SearchBar.d.ts +8 -0
  47. package/dist/ui/components/SearchBar.js +11 -0
  48. package/dist/ui/components/SearchResults.d.ts +9 -0
  49. package/dist/ui/components/SearchResults.js +18 -0
  50. package/dist/ui/components/TaskItem.d.ts +6 -1
  51. package/dist/ui/components/TaskItem.js +3 -2
  52. package/dist/ui/theme/themes.d.ts +12 -0
  53. package/dist/ui/theme/themes.js +495 -3
  54. package/dist/ui/theme/types.d.ts +1 -1
  55. package/package.json +2 -1
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import { type ViewMode } from '../config.js';
3
+ interface ModeSelectorProps {
4
+ onSelect: (mode: ViewMode) => void;
5
+ }
6
+ export declare function ModeSelector({ onSelect }: ModeSelectorProps): React.ReactElement;
7
+ 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 { getViewMode } from '../config.js';
5
+ const VALID_VIEW_MODES = ['gtd', 'kanban'];
6
+ const modeDisplayNames = {
7
+ gtd: 'GTD (Getting Things Done)',
8
+ kanban: 'Kanban Board',
9
+ };
10
+ const modeDescriptions = {
11
+ gtd: 'Classic GTD workflow with Inbox, Next, Waiting, Someday lists',
12
+ kanban: '3-column kanban board view',
13
+ };
14
+ export function ModeSelector({ onSelect }) {
15
+ const currentMode = getViewMode();
16
+ const initialIndex = VALID_VIEW_MODES.indexOf(currentMode);
17
+ const [selectedIndex, setSelectedIndex] = useState(initialIndex >= 0 ? initialIndex : 0);
18
+ useInput((input, key) => {
19
+ // j or down arrow: move down
20
+ if (input === 'j' || key.downArrow) {
21
+ setSelectedIndex((prev) => (prev < VALID_VIEW_MODES.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_VIEW_MODES.length - 1));
26
+ }
27
+ // Enter: select
28
+ if (key.return) {
29
+ onSelect(VALID_VIEW_MODES[selectedIndex]);
30
+ }
31
+ });
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) => {
33
+ const isSelected = index === selectedIndex;
34
+ const isCurrent = mode === currentMode;
35
+ 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));
36
+ }) })] }));
37
+ }
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface SetupWizardProps {
3
+ onComplete: () => void;
4
+ }
5
+ export declare function SetupWizard({ onComplete }: SetupWizardProps): React.ReactElement;
6
+ export {};
@@ -0,0 +1,321 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text, useInput, useApp } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import { themes, VALID_THEMES } from './theme/themes.js';
6
+ import { saveConfig } from '../config.js';
7
+ import { t } from '../i18n/index.js';
8
+ const LOCALES = [
9
+ { value: 'en', label: 'English', desc: 'English language' },
10
+ { value: 'ja', label: '日本語', desc: 'Japanese language' },
11
+ ];
12
+ const VIEW_MODES = [
13
+ { value: 'gtd', labelKey: 'gtd', descKey: 'gtdDesc' },
14
+ { value: 'kanban', labelKey: 'kanban', descKey: 'kanbanDesc' },
15
+ ];
16
+ const DB_OPTIONS = [
17
+ { value: 'local', labelKey: 'local', descKey: 'localDesc' },
18
+ { value: 'turso', labelKey: 'turso', descKey: 'tursoDesc' },
19
+ ];
20
+ export function SetupWizard({ onComplete }) {
21
+ const { exit } = useApp();
22
+ const [step, setStep] = useState('welcome');
23
+ const [state, setState] = useState({
24
+ locale: 'en',
25
+ theme: 'modern',
26
+ viewMode: 'gtd',
27
+ useTurso: false,
28
+ tursoUrl: '',
29
+ tursoToken: '',
30
+ });
31
+ // Selection indexes for various steps
32
+ const [languageIndex, setLanguageIndex] = useState(0);
33
+ const [themeIndex, setThemeIndex] = useState(0);
34
+ const [viewModeIndex, setViewModeIndex] = useState(0);
35
+ const [dbIndex, setDbIndex] = useState(0);
36
+ // Input values for Turso
37
+ const [inputValue, setInputValue] = useState('');
38
+ const [inputError, setInputError] = useState(null);
39
+ // Get translations - use selected locale for setup wizard
40
+ const [i18n, setI18n] = useState(t());
41
+ // Update translations when locale changes
42
+ useEffect(() => {
43
+ // Re-get translations when state.locale changes
44
+ // We need to temporarily set the config locale to get correct translations
45
+ const translations = t();
46
+ setI18n(translations);
47
+ }, []);
48
+ // Get setup translations based on selected locale
49
+ const getSetupI18n = () => {
50
+ // Use dynamic import to get correct language translations
51
+ if (state.locale === 'ja') {
52
+ return {
53
+ welcome: {
54
+ title: 'Floqへようこそ!',
55
+ subtitle: 'タスクマネージャーを設定しましょう',
56
+ instruction: '任意のキーを押して設定を開始...',
57
+ },
58
+ language: {
59
+ title: '言語を選択',
60
+ hint: 'j/k: 選択, Enter: 確定, Esc: 戻る',
61
+ },
62
+ theme: {
63
+ title: 'テーマを選択',
64
+ hint: 'j/k: 選択, Enter: 確定, Esc: 戻る',
65
+ },
66
+ viewMode: {
67
+ title: '表示モードを選択',
68
+ hint: 'j/k: 選択, Enter: 確定, Esc: 戻る',
69
+ gtd: 'GTD (Getting Things Done)',
70
+ gtdDesc: 'Inbox、次のアクション、連絡待ち、いつかやるリストによるクラシックGTDワークフロー',
71
+ kanban: 'Kanbanボード',
72
+ kanbanDesc: '3カラムのKanbanボード表示',
73
+ },
74
+ database: {
75
+ title: 'データベースモードを選択',
76
+ local: 'ローカル',
77
+ localDesc: 'このデバイスにデータをローカル保存',
78
+ turso: 'Turso Cloud',
79
+ tursoDesc: 'Turso経由でデバイス間でデータを同期',
80
+ hint: 'j/k: 選択, Enter: 確定, Esc: 戻る',
81
+ },
82
+ turso: {
83
+ urlPrompt: 'TursoデータベースURL',
84
+ urlPlaceholder: 'libsql://your-db.turso.io',
85
+ urlError: 'URLはlibsql://で始まる必要があります',
86
+ tokenPrompt: 'Turso認証トークン',
87
+ tokenError: 'トークンは空にできません',
88
+ inputHint: 'Enter: 確定, Esc: 戻る',
89
+ },
90
+ complete: {
91
+ title: '設定完了!',
92
+ summary: '設定内容:',
93
+ language: '言語',
94
+ theme: 'テーマ',
95
+ viewMode: '表示モード',
96
+ database: 'データベース',
97
+ confirm: 'Enterを押してFloqを開始...',
98
+ },
99
+ };
100
+ }
101
+ return i18n.setup;
102
+ };
103
+ const setupI18n = getSetupI18n();
104
+ const handleSave = () => {
105
+ const tursoConfig = state.useTurso
106
+ ? { url: state.tursoUrl, authToken: state.tursoToken }
107
+ : undefined;
108
+ saveConfig({
109
+ locale: state.locale,
110
+ theme: state.theme,
111
+ viewMode: state.viewMode,
112
+ turso: tursoConfig,
113
+ });
114
+ onComplete();
115
+ };
116
+ const goToPrevStep = () => {
117
+ switch (step) {
118
+ case 'welcome':
119
+ exit();
120
+ break;
121
+ case 'language':
122
+ setStep('welcome');
123
+ break;
124
+ case 'theme':
125
+ setStep('language');
126
+ break;
127
+ case 'viewMode':
128
+ setStep('theme');
129
+ break;
130
+ case 'database':
131
+ setStep('viewMode');
132
+ break;
133
+ case 'turso-url':
134
+ setStep('database');
135
+ setInputValue('');
136
+ setInputError(null);
137
+ break;
138
+ case 'turso-token':
139
+ setStep('turso-url');
140
+ setInputValue(state.tursoUrl);
141
+ setInputError(null);
142
+ break;
143
+ case 'complete':
144
+ setStep('database');
145
+ break;
146
+ }
147
+ };
148
+ useInput((input, key) => {
149
+ // Welcome step - any key continues
150
+ if (step === 'welcome') {
151
+ if (key.escape) {
152
+ exit();
153
+ }
154
+ else {
155
+ setStep('language');
156
+ }
157
+ return;
158
+ }
159
+ // Input steps (turso-url and turso-token)
160
+ if (step === 'turso-url' || step === 'turso-token') {
161
+ if (key.escape) {
162
+ goToPrevStep();
163
+ }
164
+ return;
165
+ }
166
+ // Complete step - Enter saves
167
+ if (step === 'complete') {
168
+ if (key.return) {
169
+ handleSave();
170
+ }
171
+ else if (key.escape) {
172
+ goToPrevStep();
173
+ }
174
+ return;
175
+ }
176
+ // Selection steps
177
+ if (key.escape) {
178
+ goToPrevStep();
179
+ return;
180
+ }
181
+ // j/k or arrow navigation
182
+ if (input === 'j' || key.downArrow) {
183
+ switch (step) {
184
+ case 'language':
185
+ setLanguageIndex((prev) => (prev < LOCALES.length - 1 ? prev + 1 : 0));
186
+ break;
187
+ case 'theme':
188
+ setThemeIndex((prev) => (prev < VALID_THEMES.length - 1 ? prev + 1 : 0));
189
+ break;
190
+ case 'viewMode':
191
+ setViewModeIndex((prev) => (prev < VIEW_MODES.length - 1 ? prev + 1 : 0));
192
+ break;
193
+ case 'database':
194
+ setDbIndex((prev) => (prev < DB_OPTIONS.length - 1 ? prev + 1 : 0));
195
+ break;
196
+ }
197
+ return;
198
+ }
199
+ if (input === 'k' || key.upArrow) {
200
+ switch (step) {
201
+ case 'language':
202
+ setLanguageIndex((prev) => (prev > 0 ? prev - 1 : LOCALES.length - 1));
203
+ break;
204
+ case 'theme':
205
+ setThemeIndex((prev) => (prev > 0 ? prev - 1 : VALID_THEMES.length - 1));
206
+ break;
207
+ case 'viewMode':
208
+ setViewModeIndex((prev) => (prev > 0 ? prev - 1 : VIEW_MODES.length - 1));
209
+ break;
210
+ case 'database':
211
+ setDbIndex((prev) => (prev > 0 ? prev - 1 : DB_OPTIONS.length - 1));
212
+ break;
213
+ }
214
+ return;
215
+ }
216
+ // Enter to confirm selection
217
+ if (key.return) {
218
+ switch (step) {
219
+ case 'language':
220
+ setState((prev) => ({ ...prev, locale: LOCALES[languageIndex].value }));
221
+ setStep('theme');
222
+ break;
223
+ case 'theme':
224
+ setState((prev) => ({ ...prev, theme: VALID_THEMES[themeIndex] }));
225
+ setStep('viewMode');
226
+ break;
227
+ case 'viewMode':
228
+ setState((prev) => ({ ...prev, viewMode: VIEW_MODES[viewModeIndex].value }));
229
+ setStep('database');
230
+ break;
231
+ case 'database':
232
+ const useTurso = DB_OPTIONS[dbIndex].value === 'turso';
233
+ setState((prev) => ({ ...prev, useTurso }));
234
+ if (useTurso) {
235
+ setStep('turso-url');
236
+ }
237
+ else {
238
+ setStep('complete');
239
+ }
240
+ break;
241
+ }
242
+ }
243
+ });
244
+ const handleTursoUrlSubmit = (value) => {
245
+ if (!value.startsWith('libsql://')) {
246
+ setInputError(setupI18n.turso.urlError);
247
+ return;
248
+ }
249
+ setState((prev) => ({ ...prev, tursoUrl: value }));
250
+ setInputValue('');
251
+ setInputError(null);
252
+ setStep('turso-token');
253
+ };
254
+ const handleTursoTokenSubmit = (value) => {
255
+ if (!value.trim()) {
256
+ setInputError(setupI18n.turso.tokenError);
257
+ return;
258
+ }
259
+ setState((prev) => ({ ...prev, tursoToken: value.trim() }));
260
+ setInputValue('');
261
+ setInputError(null);
262
+ setStep('complete');
263
+ };
264
+ // Render Welcome step
265
+ if (step === 'welcome') {
266
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, alignItems: "center", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: `
267
+ ███████╗██╗ ██████╗ ██████╗
268
+ ██╔════╝██║ ██╔═══██╗██╔═══██╗
269
+ █████╗ ██║ ██║ ██║██║ ██║
270
+ ██╔══╝ ██║ ██║ ██║██║▄▄ ██║
271
+ ██║ ███████╗╚██████╔╝╚██████╔╝
272
+ ╚═╝ ╚══════╝ ╚═════╝ ╚══▀▀═╝
273
+ ` }) }), _jsx(Text, { bold: true, color: "green", children: setupI18n.welcome.title }), _jsx(Text, { color: "white", children: setupI18n.welcome.subtitle }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { dimColor: true, children: setupI18n.welcome.instruction }) })] }));
274
+ }
275
+ // Render Language selection
276
+ if (step === 'language') {
277
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: setupI18n.language.title }), _jsx(Text, { dimColor: true, children: setupI18n.language.hint }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: LOCALES.map((locale, index) => {
278
+ const isSelected = index === languageIndex;
279
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: isSelected ? 'cyan' : undefined, children: [isSelected ? '› ' : ' ', locale.label] }), _jsxs(Text, { dimColor: true, children: [" ", locale.desc] })] }, locale.value));
280
+ }) })] }));
281
+ }
282
+ // Render Theme selection
283
+ if (step === 'theme') {
284
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: setupI18n.theme.title }), _jsx(Text, { dimColor: true, children: setupI18n.theme.hint }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: VALID_THEMES.map((themeName, index) => {
285
+ const theme = themes[themeName];
286
+ const isSelected = index === themeIndex;
287
+ return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? 'cyan' : undefined, children: [isSelected ? '› ' : ' ', theme.displayName] }) }, themeName));
288
+ }) })] }));
289
+ }
290
+ // Render View Mode selection
291
+ if (step === 'viewMode') {
292
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: setupI18n.viewMode.title }), _jsx(Text, { dimColor: true, children: setupI18n.viewMode.hint }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: VIEW_MODES.map((mode, index) => {
293
+ const isSelected = index === viewModeIndex;
294
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: isSelected ? 'cyan' : undefined, children: [isSelected ? '› ' : ' ', setupI18n.viewMode[mode.labelKey]] }), _jsxs(Text, { dimColor: true, children: [" ", setupI18n.viewMode[mode.descKey]] })] }, mode.value));
295
+ }) })] }));
296
+ }
297
+ // Render Database selection
298
+ if (step === 'database') {
299
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: setupI18n.database.title }), _jsx(Text, { dimColor: true, children: setupI18n.database.hint }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: DB_OPTIONS.map((option, index) => {
300
+ const isSelected = index === dbIndex;
301
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: isSelected ? 'cyan' : undefined, children: [isSelected ? '› ' : ' ', setupI18n.database[option.labelKey]] }), _jsxs(Text, { dimColor: true, children: [" ", setupI18n.database[option.descKey]] })] }, option.value));
302
+ }) })] }));
303
+ }
304
+ // Render Turso URL input
305
+ if (step === 'turso-url') {
306
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: setupI18n.turso.urlPrompt }), _jsx(Text, { dimColor: true, children: setupI18n.turso.inputHint }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "\u203A " }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleTursoUrlSubmit, placeholder: setupI18n.turso.urlPlaceholder })] }), inputError && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", children: inputError }) }))] }));
307
+ }
308
+ // Render Turso Token input
309
+ if (step === 'turso-token') {
310
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: setupI18n.turso.tokenPrompt }), _jsx(Text, { dimColor: true, children: setupI18n.turso.inputHint }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "\u203A " }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleTursoTokenSubmit, placeholder: "" })] }), inputError && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", children: inputError }) }))] }));
311
+ }
312
+ // Render Complete step
313
+ if (step === 'complete') {
314
+ const selectedTheme = themes[state.theme];
315
+ const dbLabel = state.useTurso
316
+ ? `Turso (${state.tursoUrl})`
317
+ : setupI18n.database.local;
318
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: "green", children: setupI18n.complete.title }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { bold: true, children: setupI18n.complete.summary }) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { children: [_jsxs(Text, { color: "cyan", children: [setupI18n.complete.language, ":"] }), " ", LOCALES.find(l => l.value === state.locale)?.label] }), _jsxs(Text, { children: [_jsxs(Text, { color: "cyan", children: [setupI18n.complete.theme, ":"] }), " ", selectedTheme.displayName] }), _jsxs(Text, { children: [_jsxs(Text, { color: "cyan", children: [setupI18n.complete.viewMode, ":"] }), " ", setupI18n.viewMode[state.viewMode === 'gtd' ? 'gtd' : 'kanban']] }), _jsxs(Text, { children: [_jsxs(Text, { color: "cyan", children: [setupI18n.complete.database, ":"] }), " ", dbLabel] })] }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { dimColor: true, children: setupI18n.complete.confirm }) })] }));
319
+ }
320
+ return _jsx(Text, { children: "Unknown step" });
321
+ }
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { type ThemeName } from '../config.js';
2
+ import type { ThemeName } from './theme/types.js';
3
3
  interface ThemeSelectorProps {
4
4
  onSelect: (theme: ThemeName) => void;
5
5
  }
@@ -1,17 +1,30 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import { Select } from '@inkjs/ui';
2
+ import { useState } from 'react';
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
6
  export function ThemeSelector({ onSelect }) {
7
7
  const currentTheme = getThemeName();
8
- const options = VALID_THEMES.map((themeName) => {
9
- const theme = themes[themeName];
10
- const isCurrent = themeName === currentTheme;
11
- return {
12
- label: `${theme.displayName}${isCurrent ? ' (current)' : ''}`,
13
- value: themeName,
14
- };
8
+ const initialIndex = VALID_THEMES.indexOf(currentTheme);
9
+ const [selectedIndex, setSelectedIndex] = useState(initialIndex >= 0 ? initialIndex : 0);
10
+ useInput((input, key) => {
11
+ // j or down arrow: move down
12
+ if (input === 'j' || key.downArrow) {
13
+ setSelectedIndex((prev) => (prev < VALID_THEMES.length - 1 ? prev + 1 : 0));
14
+ }
15
+ // k or up arrow: move up
16
+ if (input === 'k' || key.upArrow) {
17
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : VALID_THEMES.length - 1));
18
+ }
19
+ // Enter: select
20
+ if (key.return) {
21
+ onSelect(VALID_THEMES[selectedIndex]);
22
+ }
15
23
  });
16
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Select a theme:" }), _jsx(Box, { marginTop: 1, children: _jsx(Select, { options: options, onChange: (value) => onSelect(value) }) })] }));
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) => {
25
+ const theme = themes[themeName];
26
+ const isSelected = index === selectedIndex;
27
+ const isCurrent = themeName === currentTheme;
28
+ return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? 'cyan' : undefined, children: [isSelected ? '› ' : ' ', theme.displayName, isCurrent ? ' (current)' : ''] }) }, themeName));
29
+ }) })] }));
17
30
  }
@@ -1,10 +1,11 @@
1
1
  import React from 'react';
2
- interface FunctionKey {
2
+ interface ActionKey {
3
3
  key: string;
4
4
  label: string;
5
5
  }
6
- interface FunctionKeyBarProps {
7
- keys?: FunctionKey[];
6
+ interface ActionKeyBarProps {
7
+ keys?: ActionKey[];
8
8
  }
9
- export declare function FunctionKeyBar({ keys }: FunctionKeyBarProps): React.ReactElement;
9
+ export declare function ActionKeyBar({ keys }: ActionKeyBarProps): React.ReactElement;
10
+ export declare const FunctionKeyBar: typeof ActionKeyBar;
10
11
  export {};
@@ -1,19 +1,23 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { useTheme } from '../theme/index.js';
4
- const DEFAULT_KEYS = [
5
- { key: 'F1', label: 'Help' },
6
- { key: 'F2', label: 'Add' },
7
- { key: 'F3', label: 'Done' },
8
- { key: 'F4', label: 'Next' },
9
- { key: 'F5', label: 'Rfrsh' },
10
- { key: 'F6', label: '' },
11
- { key: 'F7', label: '' },
12
- { key: 'F8', label: '' },
13
- { key: 'F9', label: '' },
14
- { key: 'F10', label: 'Quit' },
15
- ];
16
- export function FunctionKeyBar({ keys = DEFAULT_KEYS }) {
4
+ import { t } from '../../i18n/index.js';
5
+ export function ActionKeyBar({ keys }) {
17
6
  const theme = useTheme();
18
- return (_jsx(Box, { children: keys.map((fn, index) => (_jsxs(Box, { marginRight: 0, children: [_jsx(Text, { backgroundColor: theme.colors.fnKeyLabel, color: "white", bold: true, children: fn.key }), _jsx(Text, { backgroundColor: theme.colors.fnKeyText, color: "black", children: fn.label.padEnd(5, ' ') })] }, index))) }));
7
+ const i18n = t();
8
+ const kb = i18n.tui.keyBar;
9
+ const defaultKeys = [
10
+ { key: 'a', label: kb.add },
11
+ { key: 'd', label: kb.done },
12
+ { key: 'n', label: kb.next },
13
+ { key: 's', label: kb.someday },
14
+ { key: 'i', label: kb.inbox },
15
+ { key: 'p', label: kb.project },
16
+ { key: '?', label: kb.help },
17
+ { key: 'q', label: kb.quit },
18
+ ];
19
+ const displayKeys = keys || defaultKeys;
20
+ return (_jsx(Box, { children: displayKeys.map((action, index) => (_jsxs(Box, { marginRight: 1, children: [_jsxs(Text, { color: theme.colors.fnKeyLabel, bold: true, children: ["[", action.key, "]"] }), _jsx(Text, { color: theme.colors.fnKeyText, children: action.label })] }, index))) }));
19
21
  }
22
+ // Alias for backward compatibility
23
+ export const FunctionKeyBar = ActionKeyBar;
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  interface HelpModalProps {
3
3
  onClose: () => void;
4
+ isKanban?: boolean;
4
5
  }
5
- export declare function HelpModal({ onClose }: HelpModalProps): React.ReactElement;
6
+ export declare function HelpModal({ onClose, isKanban }: HelpModalProps): React.ReactElement;
6
7
  export {};
@@ -1,11 +1,125 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useMemo } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
3
4
  import { t } from '../../i18n/index.js';
4
5
  import { useTheme } from '../theme/index.js';
5
- export function HelpModal({ onClose }) {
6
+ import { parseChangelog } from '../../changelog.js';
7
+ const VISIBLE_LINES = 12;
8
+ export function HelpModal({ onClose, isKanban = false }) {
9
+ const [activeTab, setActiveTab] = useState('keybindings');
10
+ const [scrollOffset, setScrollOffset] = useState(0);
6
11
  const i18n = t();
12
+ const theme = useTheme();
7
13
  const help = i18n.tui.help;
14
+ // Get changelog items for scroll calculation
15
+ const changelogItems = useMemo(() => {
16
+ const changelog = parseChangelog();
17
+ const items = [];
18
+ for (const entry of changelog.entries.slice(0, 5)) {
19
+ items.push({ type: 'version', content: entry.version, date: entry.date });
20
+ for (const section of entry.sections) {
21
+ items.push({ type: 'section', content: section.type });
22
+ for (const item of section.items) {
23
+ items.push({ type: 'item', content: item });
24
+ }
25
+ }
26
+ }
27
+ return items;
28
+ }, []);
29
+ const maxScroll = Math.max(0, changelogItems.length - VISIBLE_LINES);
30
+ useInput((input, key) => {
31
+ // Tab key detection (key.tab or raw tab character)
32
+ if (key.tab || input === '\t') {
33
+ setActiveTab(prev => prev === 'keybindings' ? 'whatsNew' : 'keybindings');
34
+ setScrollOffset(0);
35
+ return;
36
+ }
37
+ // Number keys for direct tab access
38
+ if (input === '1') {
39
+ setActiveTab('keybindings');
40
+ setScrollOffset(0);
41
+ return;
42
+ }
43
+ if (input === '2') {
44
+ setActiveTab('whatsNew');
45
+ setScrollOffset(0);
46
+ return;
47
+ }
48
+ // Arrow keys and h/l for tab switching
49
+ if (input === 'h' || key.leftArrow) {
50
+ setActiveTab('keybindings');
51
+ setScrollOffset(0);
52
+ return;
53
+ }
54
+ if (input === 'l' || key.rightArrow) {
55
+ setActiveTab('whatsNew');
56
+ setScrollOffset(0);
57
+ return;
58
+ }
59
+ // Scroll in What's New tab
60
+ if (activeTab === 'whatsNew') {
61
+ if (input === 'j' || key.downArrow) {
62
+ setScrollOffset(prev => Math.min(prev + 1, maxScroll));
63
+ return;
64
+ }
65
+ if (input === 'k' || key.upArrow) {
66
+ setScrollOffset(prev => Math.max(prev - 1, 0));
67
+ return;
68
+ }
69
+ }
70
+ // Close modal
71
+ if (key.escape || key.return || input === 'q' || input === ' ') {
72
+ onClose();
73
+ }
74
+ });
75
+ const formatTitle = (title) => theme.style.headerUppercase ? title.toUpperCase() : title;
76
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.modal, borderColor: theme.colors.borderActive, paddingX: 2, paddingY: 1, children: [_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsxs(Box, { children: [_jsxs(Text, { bold: activeTab === 'keybindings', color: activeTab === 'keybindings' ? theme.colors.secondary : theme.colors.textMuted, inverse: activeTab === 'keybindings', children: [' ', formatTitle(help.keybindingsTab), ' '] }), _jsx(Text, { color: theme.colors.textMuted, children: " \u2502 " }), _jsxs(Text, { bold: activeTab === 'whatsNew', color: activeTab === 'whatsNew' ? theme.colors.secondary : theme.colors.textMuted, inverse: activeTab === 'whatsNew', children: [' ', formatTitle(help.whatsNewTab), ' '] })] }) }), activeTab === 'keybindings' ? (isKanban ? (_jsx(KanbanKeybindingsContent, {})) : (_jsx(GTDKeybindingsContent, {}))) : (_jsx(WhatsNewContent, { items: changelogItems, scrollOffset: scrollOffset, visibleLines: VISIBLE_LINES, maxScroll: maxScroll })), _jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { color: theme.colors.textMuted, children: activeTab === 'whatsNew' && maxScroll > 0
77
+ ? `j/k: scroll | ${help.tabHint} | ${help.closeHint}`
78
+ : `${help.tabHint} | ${help.closeHint}` }) })] }));
79
+ }
80
+ function GTDKeybindingsContent() {
81
+ const i18n = t();
82
+ const help = i18n.tui.help;
83
+ const theme = useTheme();
84
+ const formatTitle = (title) => theme.style.headerUppercase ? title.toUpperCase() : title;
85
+ 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] })] })] })] }));
86
+ }
87
+ function KanbanKeybindingsContent() {
88
+ const i18n = t();
89
+ const help = i18n.tui.kanbanHelp;
90
+ const theme = useTheme();
91
+ const formatTitle = (title) => theme.style.headerUppercase ? title.toUpperCase() : title;
92
+ 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] })] })] })] }));
93
+ }
94
+ function WhatsNewContent({ items, scrollOffset, visibleLines, maxScroll }) {
95
+ const i18n = t();
96
+ const whatsNew = i18n.tui.whatsNew;
8
97
  const theme = useTheme();
9
98
  const formatTitle = (title) => theme.style.headerUppercase ? title.toUpperCase() : title;
10
- return (_jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.modal, borderColor: theme.colors.borderActive, paddingX: 2, paddingY: 1, children: [_jsx(Box, { justifyContent: "center", marginBottom: 1, children: _jsx(Text, { bold: true, color: theme.colors.secondary, children: formatTitle(help.title) }) }), _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-5" }), " ", 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: "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.showHelp] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "q" }), " ", help.quit] })] })] }), _jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { color: theme.colors.textMuted, children: help.closeHint }) })] }));
99
+ const getSectionLabel = (type) => {
100
+ const labels = {
101
+ Added: whatsNew.added,
102
+ Changed: whatsNew.changed,
103
+ Deprecated: whatsNew.deprecated,
104
+ Removed: whatsNew.removed,
105
+ Fixed: whatsNew.fixed,
106
+ Security: whatsNew.security,
107
+ };
108
+ return labels[type] || type;
109
+ };
110
+ if (items.length === 0) {
111
+ return (_jsx(Box, { justifyContent: "center", paddingY: 2, children: _jsx(Text, { color: theme.colors.textMuted, children: whatsNew.noChanges }) }));
112
+ }
113
+ const visibleItems = items.slice(scrollOffset, scrollOffset + visibleLines);
114
+ const showScrollUp = scrollOffset > 0;
115
+ const showScrollDown = scrollOffset < maxScroll;
116
+ return (_jsxs(Box, { flexDirection: "column", children: [showScrollUp && (_jsx(Text, { color: theme.colors.textMuted, children: " \u25B2 scroll up" })), visibleItems.map((item, index) => {
117
+ if (item.type === 'version') {
118
+ return (_jsxs(Text, { bold: true, color: theme.colors.accent, children: ["v", item.content, " ", item.date && _jsxs(Text, { color: theme.colors.textMuted, children: ["(", item.date, ")"] })] }, `${item.content}-${index}`));
119
+ }
120
+ if (item.type === 'section') {
121
+ return (_jsxs(Text, { color: theme.colors.secondary, children: [' ', formatTitle(getSectionLabel(item.content)), ":"] }, `${item.content}-${index}`));
122
+ }
123
+ return (_jsxs(Text, { color: theme.colors.text, children: [' ', "\u2022 ", item.content] }, `${item.content}-${index}`));
124
+ }), showScrollDown && (_jsx(Text, { color: theme.colors.textMuted, children: " \u25BC scroll down" }))] }));
11
125
  }
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface KanbanBoardProps {
3
+ onSwitchToGtd?: () => void;
4
+ }
5
+ export declare function KanbanBoard({ onSwitchToGtd }: KanbanBoardProps): React.ReactElement;
6
+ export {};