floq 0.4.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.
@@ -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 {};
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useTheme } from '../theme/index.js';
4
+ /**
5
+ * Dragon Quest style multi-window layout
6
+ *
7
+ * ╭────────────────────────────────────────────╮
8
+ * │ FLOQ - GTD Manager │
9
+ * ╰────────────────────────────────────────────╯
10
+ * ╭─ コマンド ─╮ ╭─ Inbox ──────────────────────╮
11
+ * │ ▶ Inbox │ │ ▶ タスク1 │
12
+ * │ 次 │ │ タスク2 │
13
+ * │ 待ち │ │ タスク3 │
14
+ * │ いつか │ ╰────────────────────────────────╯
15
+ * │ 完了 │
16
+ * ╰───────────╯
17
+ */
18
+ export function DQLayout({ title, subtitle, menuTitle, menuItems, contentTitle, children, statusTitle, statusItems, footer, }) {
19
+ const theme = useTheme();
20
+ const borderColor = theme.colors.border;
21
+ const activeColor = theme.colors.borderActive;
22
+ // Render a window with title on border
23
+ const renderWindow = (windowTitle, content, options = {}) => {
24
+ const { width, minHeight = 3, active = false, flexGrow } = options;
25
+ const color = active ? activeColor : borderColor;
26
+ const titleDisplay = ` ${windowTitle} `;
27
+ const minW = width || 20;
28
+ return (_jsxs(Box, { flexDirection: "column", width: width, minHeight: minHeight, flexGrow: flexGrow, children: [_jsxs(Box, { children: [_jsx(Text, { color: color, children: "\u256D\u2500\u2500" }), _jsx(Text, { color: theme.colors.text, bold: true, children: titleDisplay }), _jsxs(Text, { color: color, children: ['─'.repeat(Math.max(minW - titleDisplay.length - 6, 2)), "\u256E"] })] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [_jsx(Text, { color: color, children: "\u2502 " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: content }), _jsx(Text, { color: color, children: " \u2502" })] }) }), _jsx(Box, { children: _jsxs(Text, { color: color, children: ["\u2570", '─'.repeat(Math.max(minW - 2, 2)), "\u256F"] }) })] }));
29
+ };
30
+ // Render header window
31
+ const renderHeader = () => {
32
+ const headerWidth = 50;
33
+ const padding = Math.floor((headerWidth - title.length - 4) / 2);
34
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { children: _jsxs(Text, { color: borderColor, children: ["\u256D", '─'.repeat(headerWidth - 2), "\u256E"] }) }), _jsxs(Box, { children: [_jsx(Text, { color: borderColor, children: "\u2502" }), _jsxs(Text, { color: theme.colors.primary, bold: true, children: [' '.repeat(padding), title, ' '.repeat(padding)] }), _jsx(Text, { color: borderColor, children: "\u2502" })] }), subtitle && (_jsxs(Box, { children: [_jsx(Text, { color: borderColor, children: "\u2502" }), _jsxs(Text, { color: theme.colors.textMuted, children: [' '.repeat(Math.floor((headerWidth - subtitle.length - 4) / 2)), subtitle] }), _jsx(Text, { color: borderColor, children: "\u2502" })] })), _jsx(Box, { children: _jsxs(Text, { color: borderColor, children: ["\u2570", '─'.repeat(headerWidth - 2), "\u256F"] }) })] }));
35
+ };
36
+ // Render menu items
37
+ const renderMenuContent = () => (_jsx(_Fragment, { children: menuItems.map((item, index) => (_jsx(Box, { children: _jsxs(Text, { color: item.isActive ? theme.colors.textSelected : theme.colors.text, bold: item.isActive, children: [item.isActive ? theme.style.selectedPrefix : theme.style.unselectedPrefix, item.label, item.count !== undefined && ` (${item.count})`] }) }, index))) }));
38
+ // Render status window
39
+ const renderStatus = () => {
40
+ if (!statusTitle || !statusItems)
41
+ return null;
42
+ return (_jsx(Box, { marginTop: 1, children: renderWindow(statusTitle, _jsx(_Fragment, { children: statusItems.map((item, index) => (_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.textMuted, children: [item.label, ": "] }), _jsx(Text, { color: theme.colors.accent, children: item.value })] }, index))) }), { width: 20, minHeight: statusItems.length + 2 }) }));
43
+ };
44
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [renderHeader(), _jsxs(Box, { flexDirection: "row", children: [_jsxs(Box, { flexDirection: "column", marginRight: 1, children: [renderWindow(menuTitle, renderMenuContent(), {
45
+ width: 18,
46
+ minHeight: menuItems.length + 2,
47
+ active: true,
48
+ }), renderStatus()] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: renderWindow(contentTitle, children, {
49
+ minHeight: 12,
50
+ active: true,
51
+ flexGrow: 1,
52
+ }) })] }), footer && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textMuted, children: footer }) }))] }));
53
+ }
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import type { Task } from '../../db/schema.js';
3
+ import type { ProjectProgress } from './TaskItem.js';
4
+ interface DQTaskListProps {
5
+ tasks: Task[];
6
+ selectedIndex: number;
7
+ emptyMessage: string;
8
+ showProject?: boolean;
9
+ getParentProject?: (parentId: string | null) => Task | undefined;
10
+ projectProgress?: Record<string, ProjectProgress>;
11
+ isProjectTab?: boolean;
12
+ }
13
+ /**
14
+ * Dragon Quest style task list inside a window
15
+ */
16
+ export declare function DQTaskList({ tasks, selectedIndex, emptyMessage, showProject, getParentProject, projectProgress, isProjectTab, }: DQTaskListProps): React.ReactElement;
17
+ interface DQWindowFrameProps {
18
+ title: string;
19
+ children: React.ReactNode;
20
+ width?: number | string;
21
+ minHeight?: number;
22
+ active?: boolean;
23
+ flexGrow?: number;
24
+ }
25
+ /**
26
+ * Dragon Quest style window frame with title on border
27
+ */
28
+ export declare function DQWindowFrame({ title, children, width, minHeight, active, flexGrow, }: DQWindowFrameProps): React.ReactElement;
29
+ interface DQMenuProps {
30
+ title: string;
31
+ items: Array<{
32
+ key: string;
33
+ label: string;
34
+ count: number;
35
+ isActive: boolean;
36
+ }>;
37
+ width?: number;
38
+ }
39
+ /**
40
+ * Dragon Quest style menu (left side panel)
41
+ */
42
+ export declare function DQMenu({ title, items, width }: DQMenuProps): React.ReactElement;
43
+ interface DQHeaderProps {
44
+ title: string;
45
+ version: string;
46
+ dbMode: string;
47
+ contextFilter?: string | null;
48
+ }
49
+ /**
50
+ * Dragon Quest style header window
51
+ */
52
+ export declare function DQHeader({ title, version, dbMode, contextFilter }: DQHeaderProps): React.ReactElement;
53
+ export {};
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useTheme } from '../theme/index.js';
4
+ /**
5
+ * Dragon Quest style task list inside a window
6
+ */
7
+ export function DQTaskList({ tasks, selectedIndex, emptyMessage, showProject = false, getParentProject, projectProgress, isProjectTab = false, }) {
8
+ const theme = useTheme();
9
+ if (tasks.length === 0) {
10
+ return (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: emptyMessage }));
11
+ }
12
+ return (_jsx(_Fragment, { children: tasks.map((task, index) => {
13
+ const isSelected = index === selectedIndex;
14
+ const parentProject = showProject && getParentProject ? getParentProject(task.parentId) : undefined;
15
+ const progress = isProjectTab && projectProgress ? projectProgress[task.id] : undefined;
16
+ return (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix, task.title, task.waitingFor && (_jsxs(Text, { color: theme.colors.statusWaiting, children: [" [", task.waitingFor, "]"] })), task.context && (_jsxs(Text, { color: theme.colors.accent, children: [" @", task.context] })), parentProject && (_jsxs(Text, { color: theme.colors.textMuted, children: [" \u2190 ", parentProject.title] })), progress && (_jsxs(Text, { color: theme.colors.textMuted, children: [" [", progress.completed, "/", progress.total, "]"] }))] }) }, task.id));
17
+ }) }));
18
+ }
19
+ /**
20
+ * Dragon Quest style window frame with title on border
21
+ */
22
+ export function DQWindowFrame({ title, children, width, minHeight = 10, active = false, flexGrow, }) {
23
+ const theme = useTheme();
24
+ const borderColor = active ? theme.colors.borderActive : theme.colors.border;
25
+ const titleText = ` ${title} `;
26
+ return (_jsxs(Box, { flexDirection: "column", width: width, minHeight: minHeight, flexGrow: flexGrow, children: [_jsxs(Text, { color: borderColor, children: ["\u256D\u2500\u2500", _jsx(Text, { color: theme.colors.text, bold: true, children: titleText }), _jsx(Text, { color: borderColor, children: '─'.repeat(30) })] }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [_jsx(Text, { color: borderColor, children: "\u2502 " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: children }), _jsx(Text, { color: borderColor, children: " \u2502" })] }), _jsxs(Text, { color: borderColor, children: ["\u2570", '─'.repeat(titleText.length + 32), "\u256F"] })] }));
27
+ }
28
+ /**
29
+ * Dragon Quest style menu (left side panel)
30
+ */
31
+ export function DQMenu({ title, items, width = 16 }) {
32
+ const theme = useTheme();
33
+ const borderColor = theme.colors.border;
34
+ const titleText = ` ${title} `;
35
+ const innerWidth = width - 4;
36
+ return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(Text, { color: borderColor, children: ["\u256D\u2500", _jsx(Text, { color: theme.colors.text, bold: true, children: titleText }), _jsxs(Text, { color: borderColor, children: ['─'.repeat(Math.max(innerWidth - titleText.length, 0)), "\u256E"] })] }), items.map((item) => (_jsxs(Box, { children: [_jsx(Text, { color: borderColor, children: "\u2502" }), _jsxs(Text, { color: item.isActive ? theme.colors.textSelected : theme.colors.text, bold: item.isActive, children: [item.isActive ? theme.style.selectedPrefix : theme.style.unselectedPrefix, item.label] }), _jsxs(Text, { color: theme.colors.textMuted, children: ["(", item.count, ")"] }), _jsx(Text, { color: borderColor, children: "\u2502" })] }, item.key))), _jsxs(Text, { color: borderColor, children: ["\u2570", '─'.repeat(innerWidth + 2), "\u256F"] })] }));
37
+ }
38
+ /**
39
+ * Dragon Quest style header window
40
+ */
41
+ export function DQHeader({ title, version, dbMode, contextFilter }) {
42
+ const theme = useTheme();
43
+ const borderColor = theme.colors.border;
44
+ const headerText = `${title} ver.${version}`;
45
+ const width = 50;
46
+ const padding = Math.floor((width - headerText.length - 2) / 2);
47
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: borderColor, children: ["\u256D", '─'.repeat(width - 2), "\u256E"] }), _jsxs(Box, { children: [_jsx(Text, { color: borderColor, children: "\u2502" }), _jsxs(Text, { color: theme.colors.primary, bold: true, children: [' '.repeat(padding), headerText, ' '.repeat(padding)] }), _jsx(Text, { color: borderColor, children: "\u2502" })] }), _jsxs(Box, { children: [_jsx(Text, { color: borderColor, children: "\u2502" }), _jsxs(Text, { children: [' '.repeat(Math.floor((width - 20) / 2)), _jsxs(Text, { color: theme.colors.textMuted, children: ["[", dbMode, "]"] }), contextFilter !== null && contextFilter !== undefined && (_jsxs(Text, { color: theme.colors.accent, children: [" @", contextFilter === '' ? 'none' : contextFilter] }))] }), _jsx(Text, { color: borderColor, children: "\u2502" })] }), _jsxs(Text, { color: borderColor, children: ["\u2570", '─'.repeat(width - 2), "\u256F"] })] }));
48
+ }
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ interface DQWindowProps {
3
+ title?: string;
4
+ children: React.ReactNode;
5
+ width?: number | string;
6
+ height?: number | string;
7
+ minHeight?: number;
8
+ minWidth?: number;
9
+ active?: boolean;
10
+ }
11
+ /**
12
+ * Dragon Quest style window with title on the border
13
+ *
14
+ * ╭─ Title ──────────╮
15
+ * │ Content here │
16
+ * ╰──────────────────╯
17
+ */
18
+ export declare function DQWindow({ title, children, width, height, minHeight, minWidth, active, }: DQWindowProps): React.ReactElement;
19
+ export {};
@@ -0,0 +1,33 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useTheme } from '../theme/index.js';
4
+ /**
5
+ * Dragon Quest style window with title on the border
6
+ *
7
+ * ╭─ Title ──────────╮
8
+ * │ Content here │
9
+ * ╰──────────────────╯
10
+ */
11
+ export function DQWindow({ title, children, width, height, minHeight, minWidth, active = false, }) {
12
+ const theme = useTheme();
13
+ const borderColor = active ? theme.colors.borderActive : theme.colors.border;
14
+ // Build the top border with title embedded
15
+ const renderTopBorder = (contentWidth) => {
16
+ if (!title) {
17
+ // No title - just return standard corner
18
+ return null;
19
+ }
20
+ const titleText = ` ${title} `;
21
+ const titleLen = titleText.length;
22
+ // Calculate dashes needed (minimum 2 on each side of title)
23
+ const availableWidth = Math.max(contentWidth - titleLen - 4, 0);
24
+ const leftDashes = 2;
25
+ const rightDashes = Math.max(availableWidth - leftDashes, 2);
26
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: borderColor, children: ["\u256D", '─'.repeat(leftDashes)] }), _jsx(Text, { color: theme.colors.text, bold: true, children: titleText }), _jsxs(Text, { color: borderColor, children: ['─'.repeat(rightDashes), "\u256E"] })] }));
27
+ };
28
+ const renderBottomBorder = (contentWidth) => {
29
+ return (_jsx(Box, { children: _jsxs(Text, { color: borderColor, children: ["\u2570", '─'.repeat(contentWidth), "\u256F"] }) }));
30
+ };
31
+ // For DQ style, we render custom borders
32
+ return (_jsxs(Box, { flexDirection: "column", width: width, height: height, minHeight: minHeight, minWidth: minWidth, children: [title && renderTopBorder(typeof minWidth === 'number' ? minWidth - 2 : 20), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: borderColor, children: "\u2502" }), _jsx(Box, { flexDirection: "column", flexGrow: 1, paddingX: 1, children: children }), _jsx(Text, { color: borderColor, children: "\u2502" })] }), renderBottomBorder(typeof minWidth === 'number' ? minWidth - 2 : 20)] }));
33
+ }
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ type SettingsMode = 'none' | 'theme-select' | 'mode-select' | 'lang-select';
3
+ interface GtdDQProps {
4
+ onOpenSettings?: (mode: SettingsMode) => void;
5
+ }
6
+ export declare function GtdDQ({ onOpenSettings }: GtdDQProps): React.ReactElement;
7
+ export {};