floq 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  MS-DOSスタイルのテーマを備えたターミナルベースのGTD(Getting Things Done)タスクマネージャー。
6
6
 
7
+ ![Floq Demo](./assets/demo_ja.gif)
8
+
7
9
  ## 特徴
8
10
 
9
11
  - **TUIインターフェース**: Ink(CLI用React)で構築されたインタラクティブなターミナルUI
@@ -14,7 +16,8 @@ MS-DOSスタイルのテーマを備えたターミナルベースのGTD(Getti
14
16
  - **タスク検索**: `/` キーで全タスクを素早く検索
15
17
  - **コメント**: タスクにメモやコメントを追加
16
18
  - **クラウド同期**: [Turso](https://turso.tech/)のembedded replicasによるオプションの同期機能
17
- - **テーマ**: MS-DOSノスタルジックスタイルを含む複数テーマ
19
+ - **テーマ**: MS-DOSノスタルジックスタイルやドラクエRPG風を含む複数テーマ
20
+ - **スプラッシュ画面**: 起動時のスプラッシュ画面(レトロテーマではドラクエ風)
18
21
  - **多言語対応**: 英語・日本語サポート
19
22
  - **Vimスタイルナビゲーション**: hjklまたは矢印キーで操作
20
23
  - **セットアップウィザード**: 初回起動時の簡単設定
@@ -209,6 +212,12 @@ floq config mode kanban # カンバンボード
209
212
  floq config db /path/to/custom.db
210
213
  floq config db # デフォルトに戻す
211
214
 
215
+ # スプラッシュ画面設定
216
+ floq config splash # 現在の設定を表示
217
+ floq config splash 3000 # 3秒に設定
218
+ floq config splash off # スプラッシュ無効化
219
+ floq config splash key # キー入力待ちモード
220
+
212
221
  # データベースリセット(全データ削除)
213
222
  floq db reset # 確認あり
214
223
  floq db reset --force # 確認なし
@@ -254,6 +263,10 @@ floq config turso --disable
254
263
 
255
264
  26種類のテーマが利用可能。`floq config theme` でインタラクティブに選択(j/kで移動)。
256
265
 
266
+ 一部のテーマは**ドラクエRPG風UI**を採用しており、タイトル付きメッセージボックス、2カラムレイアウト、レトロなスプラッシュ画面が表示されます。DQ風テーマ: `turbo-pascal`、`msx`、`pc-98`
267
+
268
+ ![ドラクエ風UI](./assets/demo_dq.gif)
269
+
257
270
  | テーマ | 説明 |
258
271
  |--------|------|
259
272
  | `modern` | シンプルでクリーン(デフォルト) |
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  A terminal-based GTD (Getting Things Done) task manager with MS-DOS style themes.
6
6
 
7
+ ![Floq Demo](./assets/demo.gif)
8
+
7
9
  ## Features
8
10
 
9
11
  - **TUI Interface**: Interactive terminal UI built with Ink (React for CLI)
@@ -14,7 +16,8 @@ A terminal-based GTD (Getting Things Done) task manager with MS-DOS style themes
14
16
  - **Task Search**: Quick search across all tasks with `/`
15
17
  - **Comments**: Add notes and comments to tasks
16
18
  - **Cloud Sync**: Optional sync with [Turso](https://turso.tech/) using embedded replicas
17
- - **Themes**: Multiple themes including MS-DOS nostalgic styles
19
+ - **Themes**: Multiple themes including MS-DOS nostalgic styles and Dragon Quest RPG style
20
+ - **Splash Screen**: Configurable startup splash with Dragon Quest style for retro themes
18
21
  - **i18n**: English and Japanese support
19
22
  - **Vim-style Navigation**: Use hjkl or arrow keys
20
23
  - **Setup Wizard**: First-run wizard for easy configuration
@@ -209,6 +212,12 @@ floq config mode kanban # Kanban board
209
212
  floq config db /path/to/custom.db
210
213
  floq config db # Reset to default
211
214
 
215
+ # Splash screen settings
216
+ floq config splash # Show current setting
217
+ floq config splash 3000 # Set to 3 seconds
218
+ floq config splash off # Disable splash screen
219
+ floq config splash key # Wait for key press
220
+
212
221
  # Reset database (delete all data)
213
222
  floq db reset # With confirmation
214
223
  floq db reset --force # Skip confirmation
@@ -254,6 +263,10 @@ floq config turso --disable
254
263
 
255
264
  26 themes available. Use `floq config theme` for interactive selection (j/k to navigate).
256
265
 
266
+ Some themes feature a **Dragon Quest RPG style UI** with titled message boxes, 2-column layouts, and retro splash screens. Themes with DQ-style: `turbo-pascal`, `msx`, `pc-98`.
267
+
268
+ ![Dragon Quest Style UI](./assets/demo_dq.gif)
269
+
257
270
  | Theme | Description |
258
271
  |-------|-------------|
259
272
  | `modern` | Clean, minimal style (default) |
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import { listTasks, listProjects } from './commands/list.js';
7
7
  import { moveTask } from './commands/move.js';
8
8
  import { markDone } from './commands/done.js';
9
9
  import { addProject, listProjectsCommand, showProject, completeProject, } from './commands/project.js';
10
- import { showConfig, setLanguage, setDbPath, resetDbPath, setTheme, selectTheme, setViewModeCommand, selectMode, setTurso, disableTurso, syncCommand, resetDatabase } from './commands/config.js';
10
+ import { showConfig, setLanguage, setDbPath, resetDbPath, setTheme, selectTheme, setViewModeCommand, selectMode, setTurso, disableTurso, syncCommand, resetDatabase, setSplashCommand, showSplash } from './commands/config.js';
11
11
  import { addComment, listComments } from './commands/comment.js';
12
12
  import { listContexts, addContextCommand, removeContextCommand } from './commands/context.js';
13
13
  import { runSetupWizard } from './commands/setup.js';
@@ -155,6 +155,17 @@ configCmd
155
155
  process.exit(1);
156
156
  }
157
157
  });
158
+ configCmd
159
+ .command('splash [duration]')
160
+ .description('Set splash screen duration (ms, off=disable, key=wait for key)')
161
+ .action(async (duration) => {
162
+ if (duration !== undefined) {
163
+ await setSplashCommand(duration);
164
+ }
165
+ else {
166
+ await showSplash();
167
+ }
168
+ });
158
169
  // Sync command
159
170
  program
160
171
  .command('sync')
@@ -9,5 +9,7 @@ export declare function setViewModeCommand(mode: string): Promise<void>;
9
9
  export declare function selectMode(): Promise<void>;
10
10
  export declare function setTurso(url: string, token: string): Promise<void>;
11
11
  export declare function disableTurso(): Promise<void>;
12
+ export declare function setSplashCommand(duration: string): Promise<void>;
13
+ export declare function showSplash(): Promise<void>;
12
14
  export declare function syncCommand(): Promise<void>;
13
15
  export declare function resetDatabase(force: boolean): Promise<void>;
@@ -3,7 +3,7 @@ import React from 'react';
3
3
  import { createInterface } from 'readline';
4
4
  import { unlinkSync, existsSync, readdirSync } from 'fs';
5
5
  import { dirname, basename, join } from 'path';
6
- import { loadConfig, saveConfig, getDbPath, getViewMode, setViewMode, isTursoEnabled, getTursoConfig, setTursoConfig } from '../config.js';
6
+ import { loadConfig, saveConfig, getDbPath, getViewMode, setViewMode, isTursoEnabled, getTursoConfig, setTursoConfig, getSplashDuration, setSplashDuration } from '../config.js';
7
7
  import { CONFIG_FILE } from '../paths.js';
8
8
  import { ThemeSelector } from '../ui/ThemeSelector.js';
9
9
  import { ModeSelector } from '../ui/ModeSelector.js';
@@ -13,6 +13,7 @@ const VALID_LOCALES = ['en', 'ja'];
13
13
  const VALID_VIEW_MODES = ['gtd', 'kanban'];
14
14
  export async function showConfig() {
15
15
  const config = loadConfig();
16
+ const splashDuration = getSplashDuration();
16
17
  console.log('GTD CLI Configuration');
17
18
  console.log('─'.repeat(40));
18
19
  console.log(`Config file: ${CONFIG_FILE}`);
@@ -20,6 +21,7 @@ export async function showConfig() {
20
21
  console.log(`Database: ${getDbPath()}`);
21
22
  console.log(`Theme: ${config.theme || 'modern'}`);
22
23
  console.log(`View Mode: ${config.viewMode || 'gtd'}`);
24
+ console.log(`Splash: ${splashDuration === 0 ? 'disabled' : splashDuration === -1 ? 'wait for key' : `${splashDuration}ms`}`);
23
25
  console.log(`Turso: ${isTursoEnabled() ? 'enabled' : 'disabled'}`);
24
26
  if (config.db_path) {
25
27
  console.log(` (custom: ${config.db_path})`);
@@ -115,6 +117,54 @@ export async function disableTurso() {
115
117
  setTursoConfig(undefined);
116
118
  console.log('Turso sync disabled');
117
119
  }
120
+ export async function setSplashCommand(duration) {
121
+ let value;
122
+ // Handle special keywords
123
+ if (duration === 'key' || duration === 'wait') {
124
+ value = -1;
125
+ }
126
+ else if (duration === 'off' || duration === 'disable') {
127
+ value = 0;
128
+ }
129
+ else {
130
+ value = parseInt(duration, 10);
131
+ if (isNaN(value)) {
132
+ console.error(`Invalid duration: ${duration}`);
133
+ console.error('Usage: floq config splash <milliseconds|key|off>');
134
+ console.error(' off/0 = disable splash screen');
135
+ console.error(' key = wait for key press');
136
+ console.error(' positive number = duration in milliseconds');
137
+ process.exit(1);
138
+ }
139
+ if (value < 0) {
140
+ console.error('Duration must be 0 (disabled) or positive number');
141
+ console.error('Use "key" for wait-for-keypress mode');
142
+ process.exit(1);
143
+ }
144
+ }
145
+ setSplashDuration(value);
146
+ if (value === 0) {
147
+ console.log('Splash screen disabled');
148
+ }
149
+ else if (value === -1) {
150
+ console.log('Splash screen set to wait for key press');
151
+ }
152
+ else {
153
+ console.log(`Splash screen duration set to ${value}ms`);
154
+ }
155
+ }
156
+ export async function showSplash() {
157
+ const duration = getSplashDuration();
158
+ if (duration === 0) {
159
+ console.log('Splash screen: disabled');
160
+ }
161
+ else if (duration === -1) {
162
+ console.log('Splash screen: wait for key press');
163
+ }
164
+ else {
165
+ console.log(`Splash screen: ${duration}ms`);
166
+ }
167
+ }
118
168
  export async function syncCommand() {
119
169
  if (!isTursoEnabled()) {
120
170
  console.error('Turso sync is not enabled.');
package/dist/config.d.ts CHANGED
@@ -13,6 +13,7 @@ export interface Config {
13
13
  viewMode: ViewMode;
14
14
  turso?: TursoConfig;
15
15
  contexts?: string[];
16
+ splashDuration?: number;
16
17
  }
17
18
  export declare function loadConfig(): Config;
18
19
  export declare function saveConfig(updates: Partial<Config>): void;
@@ -30,3 +31,5 @@ export declare function isFirstRun(): boolean;
30
31
  export declare function getContexts(): string[];
31
32
  export declare function addContext(context: string): boolean;
32
33
  export declare function removeContext(context: string): boolean;
34
+ export declare function getSplashDuration(): number;
35
+ export declare function setSplashDuration(duration: number): void;
package/dist/config.js CHANGED
@@ -138,3 +138,16 @@ export function removeContext(context) {
138
138
  saveConfig({ contexts: newContexts });
139
139
  return true;
140
140
  }
141
+ const DEFAULT_SPLASH_DURATION = 2500; // 2.5 seconds
142
+ export function getSplashDuration() {
143
+ const duration = loadConfig().splashDuration;
144
+ // undefined means use default, 0 means disabled
145
+ if (duration === undefined) {
146
+ return DEFAULT_SPLASH_DURATION;
147
+ }
148
+ return duration;
149
+ }
150
+ export function setSplashDuration(duration) {
151
+ // Allow -1 (wait for key), 0 (disabled), or positive values
152
+ saveConfig({ splashDuration: duration >= 0 ? duration : -1 });
153
+ }
package/dist/i18n/en.d.ts CHANGED
@@ -240,6 +240,12 @@ export declare const en: {
240
240
  turso: string;
241
241
  };
242
242
  };
243
+ splash: {
244
+ welcome: string;
245
+ subtitle: string;
246
+ subtitleKanban: string;
247
+ pressKey: string;
248
+ };
243
249
  setup: {
244
250
  welcome: {
245
251
  title: string;
@@ -517,6 +523,12 @@ export type SetupTranslations = {
517
523
  confirm: string;
518
524
  };
519
525
  };
526
+ export type SplashTranslations = {
527
+ welcome: string;
528
+ subtitle: string;
529
+ subtitleKanban: string;
530
+ pressKey: string;
531
+ };
520
532
  export type Translations = {
521
533
  status: Record<string, string>;
522
534
  kanban: KanbanTranslations;
@@ -531,5 +543,6 @@ export type Translations = {
531
543
  context: Record<string, string>;
532
544
  };
533
545
  tui: TuiTranslations;
546
+ splash?: SplashTranslations;
534
547
  setup: SetupTranslations;
535
548
  };
package/dist/i18n/en.js CHANGED
@@ -255,6 +255,13 @@ export const en = {
255
255
  turso: 'turso',
256
256
  },
257
257
  },
258
+ // Splash screen (Dragon Quest style)
259
+ splash: {
260
+ welcome: 'Welcome!',
261
+ subtitle: 'Your GTD adventure begins',
262
+ subtitleKanban: 'Your Kanban adventure begins',
263
+ pressKey: '▼ Press any key',
264
+ },
258
265
  // Setup wizard
259
266
  setup: {
260
267
  welcome: {
package/dist/i18n/ja.js CHANGED
@@ -255,6 +255,13 @@ export const ja = {
255
255
  turso: 'turso',
256
256
  },
257
257
  },
258
+ // Splash screen (Dragon Quest style)
259
+ splash: {
260
+ welcome: 'ようこそ!',
261
+ subtitle: 'GTDの冒険がはじまる',
262
+ subtitleKanban: 'Kanbanの冒険がはじまる',
263
+ pressKey: '▼ なにかキーをおしてください',
264
+ },
258
265
  // Setup wizard
259
266
  setup: {
260
267
  welcome: {
package/dist/ui/App.js CHANGED
@@ -9,15 +9,18 @@ import { HelpModal } from './components/HelpModal.js';
9
9
  import { FunctionKeyBar } from './components/FunctionKeyBar.js';
10
10
  import { SearchBar } from './components/SearchBar.js';
11
11
  import { SearchResults } from './components/SearchResults.js';
12
+ import { TitledBox } from './components/TitledBox.js';
12
13
  import { SplashScreen } from './SplashScreen.js';
13
14
  import { ThemeSelector } from './ThemeSelector.js';
14
15
  import { ModeSelector } from './ModeSelector.js';
15
16
  import { LanguageSelector } from './LanguageSelector.js';
16
17
  import { getDb, schema } from '../db/index.js';
17
18
  import { t, fmt } from '../i18n/index.js';
18
- import { ThemeProvider, useTheme } from './theme/index.js';
19
- import { getThemeName, getViewMode, setThemeName, setViewMode, setLocale, isTursoEnabled, getContexts, addContext } from '../config.js';
19
+ import { ThemeProvider, useTheme, getTheme } from './theme/index.js';
20
+ import { getThemeName, getViewMode, setThemeName, setViewMode, setLocale, isTursoEnabled, getContexts, addContext, getSplashDuration } from '../config.js';
20
21
  import { KanbanBoard } from './components/KanbanBoard.js';
22
+ import { KanbanDQ } from './components/KanbanDQ.js';
23
+ import { GtdDQ } from './components/GtdDQ.js';
21
24
  import { VERSION } from '../version.js';
22
25
  import { HistoryProvider, useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from './history/index.js';
23
26
  const TABS = ['inbox', 'next', 'waiting', 'someday', 'projects', 'done'];
@@ -25,6 +28,8 @@ export function App() {
25
28
  const [themeName, setThemeNameState] = useState(getThemeName);
26
29
  const [viewMode, setViewModeState] = useState(getViewMode);
27
30
  const [settingsMode, setSettingsMode] = useState('none');
31
+ const splashDuration = getSplashDuration();
32
+ const [showSplash, setShowSplash] = useState(splashDuration !== 0);
28
33
  const [, forceUpdate] = useState({});
29
34
  const handleThemeSelect = (theme) => {
30
35
  setThemeName(theme);
@@ -44,6 +49,12 @@ export function App() {
44
49
  const handleSettingsCancel = () => {
45
50
  setSettingsMode('none');
46
51
  };
52
+ const currentTheme = getTheme(themeName);
53
+ const useDQStyle = currentTheme.uiStyle === 'titled-box';
54
+ // Show splash screen (all themes, configurable duration)
55
+ if (showSplash) {
56
+ return (_jsx(ThemeProvider, { themeName: themeName, children: _jsx(SplashScreen, { onComplete: () => setShowSplash(false), duration: splashDuration, viewMode: viewMode }) }));
57
+ }
47
58
  // Settings selector screens
48
59
  if (settingsMode === 'theme-select') {
49
60
  return (_jsx(ThemeProvider, { themeName: themeName, children: _jsx(ThemeSelector, { onSelect: handleThemeSelect, onCancel: handleSettingsCancel }) }));
@@ -54,13 +65,13 @@ export function App() {
54
65
  if (settingsMode === 'lang-select') {
55
66
  return (_jsx(ThemeProvider, { themeName: themeName, children: _jsx(LanguageSelector, { onSelect: handleLocaleSelect, onCancel: handleSettingsCancel }) }));
56
67
  }
57
- return (_jsx(ThemeProvider, { themeName: themeName, children: _jsx(HistoryProvider, { children: viewMode === 'kanban' ? (_jsx(KanbanBoard, { onOpenSettings: setSettingsMode })) : (_jsx(AppContent, { onOpenSettings: setSettingsMode })) }) }));
68
+ return (_jsx(ThemeProvider, { themeName: themeName, children: _jsx(HistoryProvider, { children: viewMode === 'kanban' ? (useDQStyle ? (_jsx(KanbanDQ, { onOpenSettings: setSettingsMode })) : (_jsx(KanbanBoard, { onOpenSettings: setSettingsMode }))) : (useDQStyle ? (_jsx(GtdDQ, { onOpenSettings: setSettingsMode })) : (_jsx(AppContent, { onOpenSettings: setSettingsMode }))) }) }));
58
69
  }
59
70
  function AppContent({ onOpenSettings }) {
60
71
  const theme = useTheme();
61
72
  const { exit } = useApp();
62
73
  const history = useHistory();
63
- const [mode, setMode] = useState('splash');
74
+ const [mode, setMode] = useState('normal');
64
75
  const [inputValue, setInputValue] = useState('');
65
76
  const [currentListIndex, setCurrentListIndex] = useState(0);
66
77
  const [selectedTaskIndex, setSelectedTaskIndex] = useState(0);
@@ -430,11 +441,6 @@ function AppContent({ onOpenSettings }) {
430
441
  }
431
442
  };
432
443
  useInput((input, key) => {
433
- // Skip splash screen on any key
434
- if (mode === 'splash') {
435
- setMode('normal');
436
- return;
437
- }
438
444
  // Handle search mode
439
445
  if (mode === 'search') {
440
446
  if (key.escape) {
@@ -978,10 +984,6 @@ function AppContent({ onOpenSettings }) {
978
984
  return;
979
985
  }
980
986
  }, { isActive: mode !== 'help' });
981
- // Splash screen
982
- if (mode === 'splash') {
983
- return _jsx(SplashScreen, { onComplete: () => setMode('normal') });
984
- }
985
987
  // Help modal overlay
986
988
  if (mode === 'help') {
987
989
  return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(HelpModal, { onClose: () => setMode('normal') }) }));
@@ -1003,11 +1005,15 @@ function AppContent({ onOpenSettings }) {
1003
1005
  }) }), 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})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] })] }), _jsx(Box, { 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) => {
1004
1006
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
1005
1007
  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));
1006
- })) }), 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) => {
1008
+ })) }), 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' && (theme.uiStyle === 'titled-box' ? (_jsx(TitledBox, { title: mode === 'project-detail' && selectedProject ? selectedProject.title : getTabLabel(currentTab), borderColor: theme.colors.border, minHeight: 10, children: currentTasks.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noTasks })) : (currentTasks.map((task, index) => {
1009
+ const parentProject = getParentProject(task.parentId);
1010
+ const progress = currentTab === 'projects' ? projectProgress[task.id] : undefined;
1011
+ return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress }, task.id));
1012
+ })) })) : (_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) => {
1007
1013
  const parentProject = getParentProject(task.parentId);
1008
1014
  const progress = currentTab === 'projects' ? projectProgress[task.id] : undefined;
1009
1015
  return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress }, task.id));
1010
- })) })), (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
1016
+ })) }))), (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
1011
1017
  ? `${i18n.tui.newTask}[${selectedProject.title}] `
1012
1018
  : 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 === 'context-filter' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.filter || 'Filter by context' }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
1013
1019
  const label = ctx === 'all'
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  interface SplashScreenProps {
3
3
  onComplete: () => void;
4
4
  duration?: number;
5
+ viewMode?: 'gtd' | 'kanban';
5
6
  }
6
- export declare function SplashScreen({ onComplete, duration }: SplashScreenProps): React.ReactElement;
7
+ export declare function SplashScreen({ onComplete, duration, viewMode }: SplashScreenProps): React.ReactElement;
7
8
  export {};
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useEffect } from 'react';
3
- import { Box, Text } from 'ink';
3
+ import { Box, Text, useInput } from 'ink';
4
4
  import { useTheme } from './theme/index.js';
5
5
  import { VERSION } from '../version.js';
6
+ import { t } from '../i18n/index.js';
6
7
  const LOGO_MODERN = `
7
8
  ███████╗██╗ ██████╗ ██████╗
8
9
  ██╔════╝██║ ██╔═══██╗██╔═══██╗
@@ -20,14 +21,88 @@ const LOGO_DOS = `
20
21
  ║ ██ ███████ ██████ ██████ ║
21
22
  ╚═══════════════════════════════════╝
22
23
  `;
24
+ // Dragon Quest style FLOQ logo with wings and sword
25
+ const FLOQ_LOGO = [
26
+ ' /\\',
27
+ ' __ / \\ __',
28
+ ' / \\ / || \\ / \\',
29
+ ' / /\\ \\ / || \\ / /\\ \\',
30
+ ' / / \\ \\ / || \\ / / \\ \\',
31
+ ' / / \\ \\_/ || \\_/ / \\ \\',
32
+ ' /_/ \\__/ || \\__/ \\_\\',
33
+ ' ||',
34
+ ' =================[##]=================',
35
+ '',
36
+ ' ######## ## ###### ######',
37
+ ' ## ## ## ## ## ##',
38
+ ' ###### ## ## ## ## ##',
39
+ ' ## ## ## ## ## # ##',
40
+ ' ## ######## ###### #### ##',
41
+ '',
42
+ ' =====================================',
43
+ ' ~ Flow Your Tasks ~',
44
+ ];
45
+ // Dragon Quest style border characters
46
+ const DQ_BORDER = {
47
+ topLeft: '╭',
48
+ topRight: '╮',
49
+ bottomLeft: '╰',
50
+ bottomRight: '╯',
51
+ horizontal: '─',
52
+ vertical: '│',
53
+ };
23
54
  const TAGLINE = 'Flow your tasks, clear your mind';
24
- export function SplashScreen({ onComplete, duration = 1500 }) {
55
+ // Dragon Quest famous quotes for splash screen
56
+ const DQ_QUOTES_JA = [
57
+ 'へんじがない。ただのしかばねのようだ。',
58
+ 'おきのどくですが ぼうけんのしょは きえてしまいました。',
59
+ 'レベルがあがった!',
60
+ 'しかし まわりこまれてしまった!',
61
+ 'たたかいに やぶれた...',
62
+ 'どうぐがいっぱいで もてません。',
63
+ 'やくそうを つかった!',
64
+ 'かいしんのいちげき!',
65
+ 'しかし なにも おこらなかった',
66
+ 'そのほうこうには すすめません',
67
+ ];
68
+ const DQ_QUOTES_EN = [
69
+ 'No response. It seems to be just a corpse.',
70
+ 'Unfortunately, your adventure log has been erased.',
71
+ 'Level up!',
72
+ 'But you were surrounded!',
73
+ 'You have been defeated...',
74
+ 'Your inventory is full.',
75
+ 'Used an herb!',
76
+ 'Critical hit!',
77
+ 'But nothing happened.',
78
+ 'You cannot go that way.',
79
+ ];
80
+ export function SplashScreen({ onComplete, duration = 1500, viewMode = 'gtd' }) {
25
81
  const [frame, setFrame] = useState(0);
26
82
  const [showTagline, setShowTagline] = useState(false);
83
+ const [blinkVisible, setBlinkVisible] = useState(true);
27
84
  const theme = useTheme();
85
+ const i18n = t();
86
+ const isDqStyle = theme.uiStyle === 'titled-box';
28
87
  const isDosStyle = theme.name !== 'modern';
29
88
  const logo = isDosStyle ? LOGO_DOS : LOGO_MODERN;
30
89
  const [filled, empty] = theme.style.loadingChars;
90
+ // Pick a random quote (stable across re-renders)
91
+ const [randomQuote] = useState(() => {
92
+ const isJapanese = i18n.splash?.welcome === 'ようこそ!';
93
+ const quotes = isJapanese ? DQ_QUOTES_JA : DQ_QUOTES_EN;
94
+ return quotes[Math.floor(Math.random() * quotes.length)];
95
+ });
96
+ // Adventure subtitle based on view mode
97
+ const adventureSubtitle = viewMode === 'kanban'
98
+ ? (i18n.splash?.subtitleKanban || 'Kanbanの冒険がはじまる')
99
+ : (i18n.splash?.subtitle || 'GTDの冒険がはじまる');
100
+ // Wait for key press mode (duration = -1)
101
+ const waitForKeyPress = duration < 0;
102
+ // Handle key press to skip splash
103
+ useInput(() => {
104
+ onComplete();
105
+ });
31
106
  useEffect(() => {
32
107
  // Animate logo appearance
33
108
  const frameInterval = setInterval(() => {
@@ -43,19 +118,43 @@ export function SplashScreen({ onComplete, duration = 1500 }) {
43
118
  const taglineTimer = setTimeout(() => {
44
119
  setShowTagline(true);
45
120
  }, 600);
46
- // Complete splash screen
47
- const completeTimer = setTimeout(() => {
48
- onComplete();
49
- }, duration);
121
+ // Blink effect for DQ style or wait-for-key mode
122
+ let blinkInterval = null;
123
+ if (isDqStyle || waitForKeyPress) {
124
+ blinkInterval = setInterval(() => {
125
+ setBlinkVisible((prev) => !prev);
126
+ }, 500);
127
+ }
128
+ // Complete splash screen (only if not waiting for key press)
129
+ let completeTimer = null;
130
+ if (!waitForKeyPress) {
131
+ completeTimer = setTimeout(() => {
132
+ onComplete();
133
+ }, duration);
134
+ }
50
135
  return () => {
51
136
  clearInterval(frameInterval);
52
137
  clearTimeout(taglineTimer);
53
- clearTimeout(completeTimer);
138
+ if (completeTimer)
139
+ clearTimeout(completeTimer);
140
+ if (blinkInterval)
141
+ clearInterval(blinkInterval);
54
142
  };
55
- }, [onComplete, duration]);
143
+ }, [onComplete, duration, isDqStyle, waitForKeyPress]);
144
+ // Dragon Quest style splash
145
+ if (isDqStyle) {
146
+ const boxWidth = 40;
147
+ const innerWidth = boxWidth - 2;
148
+ const title = 'FLOQ';
149
+ const leftDashes = 3;
150
+ const rightDashes = innerWidth - leftDashes - title.length - 2;
151
+ const shadowColor = theme.colors.muted;
152
+ return (_jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", padding: 2, children: [_jsx(Box, { flexDirection: "column", alignItems: "center", marginBottom: 1, children: FLOQ_LOGO.map((line, index) => (_jsx(Text, { color: index === 4 ? theme.colors.text : theme.colors.accent, bold: index === 4, children: line }, index))) }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.border, children: DQ_BORDER.topLeft }), _jsxs(Text, { color: theme.colors.border, children: [DQ_BORDER.horizontal.repeat(leftDashes), " "] }), _jsx(Text, { color: theme.colors.accent, bold: true, children: title }), _jsxs(Text, { color: theme.colors.border, children: [" ", DQ_BORDER.horizontal.repeat(rightDashes)] }), _jsx(Text, { color: theme.colors.border, children: DQ_BORDER.topRight }), _jsx(Text, { children: " " })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.border, children: DQ_BORDER.vertical }), _jsx(Box, { width: innerWidth, justifyContent: "center", children: _jsx(Text, { color: theme.colors.text, children: " " }) }), _jsx(Text, { color: theme.colors.border, children: DQ_BORDER.vertical }), _jsx(Text, { color: shadowColor, children: "\u2591" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.border, children: DQ_BORDER.vertical }), _jsx(Box, { width: innerWidth, justifyContent: "center", children: _jsx(Text, { color: theme.colors.text, bold: true, children: adventureSubtitle }) }), _jsx(Text, { color: theme.colors.border, children: DQ_BORDER.vertical }), _jsx(Text, { color: shadowColor, children: "\u2591" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.border, children: DQ_BORDER.vertical }), _jsx(Box, { width: innerWidth, justifyContent: "center", children: _jsx(Text, { color: theme.colors.text, children: " " }) }), _jsx(Text, { color: theme.colors.border, children: DQ_BORDER.vertical }), _jsx(Text, { color: shadowColor, children: "\u2591" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.border, children: DQ_BORDER.vertical }), _jsx(Box, { width: innerWidth, justifyContent: "center", children: _jsx(Text, { color: theme.colors.textMuted, children: randomQuote }) }), _jsx(Text, { color: theme.colors.border, children: DQ_BORDER.vertical }), _jsx(Text, { color: shadowColor, children: "\u2591" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.border, children: DQ_BORDER.vertical }), _jsx(Box, { width: innerWidth, justifyContent: "center", children: _jsx(Text, { color: theme.colors.text, children: " " }) }), _jsx(Text, { color: theme.colors.border, children: DQ_BORDER.vertical }), _jsx(Text, { color: shadowColor, children: "\u2591" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.border, children: DQ_BORDER.vertical }), _jsx(Box, { width: innerWidth, justifyContent: "center", children: _jsx(Text, { color: theme.colors.textMuted, children: blinkVisible ? (i18n.splash?.pressKey || '▼ キーを押してください') : ' ' }) }), _jsx(Text, { color: theme.colors.border, children: DQ_BORDER.vertical }), _jsx(Text, { color: shadowColor, children: "\u2591" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.border, children: DQ_BORDER.vertical }), _jsx(Box, { width: innerWidth, justifyContent: "center", children: _jsx(Text, { color: theme.colors.text, children: " " }) }), _jsx(Text, { color: theme.colors.border, children: DQ_BORDER.vertical }), _jsx(Text, { color: shadowColor, children: "\u2591" })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.border, children: DQ_BORDER.bottomLeft }), _jsx(Text, { color: theme.colors.border, children: DQ_BORDER.horizontal.repeat(innerWidth) }), _jsx(Text, { color: theme.colors.border, children: DQ_BORDER.bottomRight }), _jsx(Text, { color: shadowColor, children: "\u2591" })] }), _jsxs(Box, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: shadowColor, children: '░'.repeat(boxWidth) })] })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.colors.textMuted, children: ["VER ", VERSION] }) })] }));
153
+ }
154
+ // Standard splash (modern / DOS style)
56
155
  const logoLines = logo.split('\n');
57
156
  const visibleLines = Math.min(Math.floor(frame * logoLines.length / 10), logoLines.length);
58
- return (_jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", padding: 2, children: [_jsx(Box, { flexDirection: "column", alignItems: "center", children: logoLines.slice(0, visibleLines).map((line, index) => (_jsx(Text, { color: theme.colors.secondary, bold: true, children: line }, index))) }), showTagline && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textMuted, italic: !isDosStyle, children: isDosStyle ? `[ ${TAGLINE.toUpperCase()} ]` : TAGLINE }) })), _jsx(Box, { marginTop: 2, children: _jsx(Text, { color: theme.colors.primary, children: frame < 10
157
+ return (_jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", padding: 2, children: [_jsx(Box, { flexDirection: "column", alignItems: "center", children: logoLines.slice(0, visibleLines).map((line, index) => (_jsx(Text, { color: theme.colors.secondary, bold: true, children: line }, index))) }), showTagline && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textMuted, italic: !isDosStyle, children: isDosStyle ? `[ ${TAGLINE.toUpperCase()} ]` : TAGLINE }) })), _jsx(Box, { marginTop: 2, children: waitForKeyPress ? (_jsx(Text, { color: theme.colors.textMuted, children: blinkVisible ? (i18n.splash?.pressKey || '▼ Press any key') : ' ' })) : (_jsx(Text, { color: theme.colors.primary, children: frame < 10
59
158
  ? filled.repeat(frame) + empty.repeat(10 - frame)
60
- : filled.repeat(10) }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: isDosStyle ? theme.colors.textMuted : theme.colors.muted, children: isDosStyle ? `VER ${VERSION}` : `v${VERSION}` }) })] }));
159
+ : filled.repeat(10) })) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: isDosStyle ? theme.colors.textMuted : theme.colors.muted, children: isDosStyle ? `VER ${VERSION}` : `v${VERSION}` }) })] }));
61
160
  }
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+ interface DQLayoutProps {
3
+ title: string;
4
+ subtitle?: string;
5
+ menuTitle: string;
6
+ menuItems: Array<{
7
+ label: string;
8
+ count?: number;
9
+ isActive: boolean;
10
+ }>;
11
+ onMenuSelect?: (index: number) => void;
12
+ contentTitle: string;
13
+ children: React.ReactNode;
14
+ statusTitle?: string;
15
+ statusItems?: Array<{
16
+ label: string;
17
+ value: string;
18
+ }>;
19
+ footer?: string;
20
+ }
21
+ /**
22
+ * Dragon Quest style multi-window layout
23
+ *
24
+ * ╭────────────────────────────────────────────╮
25
+ * │ FLOQ - GTD Manager │
26
+ * ╰────────────────────────────────────────────╯
27
+ * ╭─ コマンド ─╮ ╭─ Inbox ──────────────────────╮
28
+ * │ ▶ Inbox │ │ ▶ タスク1 │
29
+ * │ 次 │ │ タスク2 │
30
+ * │ 待ち │ │ タスク3 │
31
+ * │ いつか │ ╰────────────────────────────────╯
32
+ * │ 完了 │
33
+ * ╰───────────╯
34
+ */
35
+ export declare function DQLayout({ title, subtitle, menuTitle, menuItems, contentTitle, children, statusTitle, statusItems, footer, }: DQLayoutProps): React.ReactElement;
36
+ export {};