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,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useMemo } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
4
  import { t } from '../../i18n/index.js';
@@ -6,32 +6,135 @@ import { useTheme } from '../theme/index.js';
6
6
  import { parseChangelog } from '../../changelog.js';
7
7
  import { loadConfig, isTursoEnabled, getTursoConfig, getDbPath } from '../../config.js';
8
8
  import { CONFIG_FILE, DATA_DIR } from '../../paths.js';
9
- const VISIBLE_LINES = 12;
9
+ const VISIBLE_LINES = 14;
10
10
  export function HelpModal({ onClose, isKanban = false }) {
11
11
  const [activeTab, setActiveTab] = useState('keybindings');
12
12
  const [scrollOffset, setScrollOffset] = useState(0);
13
13
  const i18n = t();
14
14
  const theme = useTheme();
15
15
  const help = i18n.tui.help;
16
- // Get changelog items for scroll calculation
17
- const changelogItems = useMemo(() => {
16
+ const formatTitle = (title) => theme.style.headerUppercase ? title.toUpperCase() : title;
17
+ // Build keybindings content
18
+ const keybindingsContent = useMemo(() => {
19
+ if (isKanban) {
20
+ const kHelp = i18n.tui.kanbanHelp;
21
+ return [
22
+ { type: 'header', value: kHelp.navigation },
23
+ { type: 'key', key: 'h/l ←/→', value: kHelp.columnSwitch },
24
+ { type: 'key', key: '1-3', value: kHelp.columnDirect },
25
+ { type: 'key', key: 'j/k ↑/↓', value: kHelp.taskSelect },
26
+ { type: 'header', value: kHelp.actions },
27
+ { type: 'key', key: 'a', value: kHelp.addTask },
28
+ { type: 'key', key: 'd', value: kHelp.completeTask },
29
+ { type: 'key', key: 'm', value: kHelp.moveRight },
30
+ { type: 'key', key: 'BS', value: kHelp.moveLeft },
31
+ { type: 'key', key: 'u', value: 'Undo' },
32
+ { type: 'key', key: 'Ctrl+r', value: 'Redo' },
33
+ { type: 'header', value: kHelp.settings },
34
+ { type: 'key', key: 'T', value: kHelp.changeTheme },
35
+ { type: 'key', key: 'V', value: kHelp.changeViewMode },
36
+ { type: 'key', key: 'L', value: kHelp.changeLanguage },
37
+ { type: 'header', value: kHelp.other },
38
+ { type: 'key', key: '/', value: kHelp.searchTasks },
39
+ { type: 'key', key: '?', value: kHelp.showHelp },
40
+ { type: 'key', key: 'q', value: kHelp.quit },
41
+ ];
42
+ }
43
+ return [
44
+ { type: 'header', value: help.navigation },
45
+ { type: 'key', key: '1-6', value: help.tabSwitch },
46
+ { type: 'key', key: 'h/l ←/→', value: help.prevNextTab },
47
+ { type: 'key', key: 'j/k ↑/↓', value: help.taskSelect },
48
+ { type: 'header', value: help.actions },
49
+ { type: 'key', key: 'a', value: help.addTask },
50
+ { type: 'key', key: 'd', value: help.completeTask },
51
+ { type: 'key', key: 'n', value: help.moveToNext },
52
+ { type: 'key', key: 's', value: help.moveToSomeday },
53
+ { type: 'key', key: 'w', value: help.moveToWaiting },
54
+ { type: 'key', key: 'i', value: help.moveToInbox },
55
+ { type: 'key', key: 'r', value: help.refresh },
56
+ { type: 'key', key: 'u', value: help.undo },
57
+ { type: 'key', key: 'Ctrl+r', value: help.redo },
58
+ { type: 'header', value: help.projects },
59
+ { type: 'key', key: 'p', value: help.makeProject },
60
+ { type: 'key', key: 'P', value: help.linkToProject },
61
+ { type: 'key', key: 'Enter', value: help.openProject },
62
+ { type: 'key', key: 'Esc/b', value: help.backFromProject },
63
+ { type: 'header', value: help.settings },
64
+ { type: 'key', key: 'T', value: help.changeTheme },
65
+ { type: 'key', key: 'V', value: help.changeViewMode },
66
+ { type: 'key', key: 'L', value: help.changeLanguage },
67
+ { type: 'header', value: help.other },
68
+ { type: 'key', key: '/', value: help.searchTasks },
69
+ { type: 'key', key: '?', value: help.showHelp },
70
+ { type: 'key', key: 'q', value: help.quit },
71
+ ];
72
+ }, [isKanban, help, i18n.tui.kanbanHelp]);
73
+ // Build info content
74
+ const infoContent = useMemo(() => {
75
+ const info = i18n.tui.info;
76
+ const config = loadConfig();
77
+ const tursoEnabled = isTursoEnabled();
78
+ const tursoConfig = tursoEnabled ? getTursoConfig() : undefined;
79
+ const dbPath = getDbPath();
80
+ const tursoUrl = tursoConfig ? (() => {
81
+ try {
82
+ return new URL(tursoConfig.url).host;
83
+ }
84
+ catch {
85
+ return tursoConfig.url;
86
+ }
87
+ })() : '';
88
+ const lines = [
89
+ { type: 'header', value: info.settings },
90
+ { type: 'key', key: info.theme, value: config.theme },
91
+ { type: 'key', key: info.language, value: config.locale },
92
+ { type: 'key', key: info.viewMode, value: config.viewMode },
93
+ { type: 'header', value: info.database },
94
+ { type: 'key', key: info.dbType, value: tursoEnabled ? info.turso : info.local },
95
+ { type: 'key', key: info.dbPath, value: dbPath },
96
+ ];
97
+ if (tursoEnabled && tursoUrl) {
98
+ lines.push({ type: 'key', key: info.tursoUrl, value: tursoUrl });
99
+ }
100
+ lines.push({ type: 'header', value: info.paths }, { type: 'key', key: info.configFile, value: CONFIG_FILE }, { type: 'key', key: info.dataDir, value: DATA_DIR });
101
+ return lines;
102
+ }, [i18n.tui.info]);
103
+ // Build changelog content
104
+ const changelogContent = useMemo(() => {
18
105
  const changelog = parseChangelog();
106
+ const whatsNew = i18n.tui.whatsNew;
19
107
  const items = [];
108
+ const getSectionLabel = (type) => {
109
+ const labels = {
110
+ Added: whatsNew.added,
111
+ Changed: whatsNew.changed,
112
+ Deprecated: whatsNew.deprecated,
113
+ Removed: whatsNew.removed,
114
+ Fixed: whatsNew.fixed,
115
+ Security: whatsNew.security,
116
+ };
117
+ return labels[type] || type;
118
+ };
20
119
  for (const entry of changelog.entries.slice(0, 5)) {
21
- items.push({ type: 'version', content: entry.version, date: entry.date });
120
+ items.push({ type: 'version', value: entry.version, date: entry.date });
22
121
  for (const section of entry.sections) {
23
- items.push({ type: 'section', content: section.type });
122
+ items.push({ type: 'section', value: getSectionLabel(section.type) });
24
123
  for (const item of section.items) {
25
- items.push({ type: 'item', content: item });
124
+ items.push({ type: 'item', value: item });
26
125
  }
27
126
  }
28
127
  }
29
128
  return items;
30
- }, []);
31
- const maxScroll = Math.max(0, changelogItems.length - VISIBLE_LINES);
129
+ }, [i18n.tui.whatsNew]);
130
+ // Get current content and max scroll
131
+ const currentContent = activeTab === 'keybindings' ? keybindingsContent
132
+ : activeTab === 'info' ? infoContent
133
+ : changelogContent;
134
+ const maxScroll = Math.max(0, currentContent.length - VISIBLE_LINES);
32
135
  const tabs = ['keybindings', 'info', 'whatsNew'];
33
136
  useInput((input, key) => {
34
- // Tab key detection (key.tab or raw tab character)
137
+ // Tab key detection
35
138
  if (key.tab || input === '\t') {
36
139
  setActiveTab(prev => {
37
140
  const currentIndex = tabs.indexOf(prev);
@@ -56,108 +159,39 @@ export function HelpModal({ onClose, isKanban = false }) {
56
159
  setScrollOffset(0);
57
160
  return;
58
161
  }
59
- // Arrow keys and h/l for tab switching
60
- if (input === 'h' || key.leftArrow) {
61
- setActiveTab(prev => {
62
- const currentIndex = tabs.indexOf(prev);
63
- return tabs[(currentIndex - 1 + tabs.length) % tabs.length];
64
- });
65
- setScrollOffset(0);
162
+ // Scroll with j/k or arrows
163
+ if (input === 'j' || key.downArrow) {
164
+ setScrollOffset(prev => Math.min(prev + 1, maxScroll));
66
165
  return;
67
166
  }
68
- if (input === 'l' || key.rightArrow) {
69
- setActiveTab(prev => {
70
- const currentIndex = tabs.indexOf(prev);
71
- return tabs[(currentIndex + 1) % tabs.length];
72
- });
73
- setScrollOffset(0);
167
+ if (input === 'k' || key.upArrow) {
168
+ setScrollOffset(prev => Math.max(prev - 1, 0));
74
169
  return;
75
170
  }
76
- // Scroll in What's New tab
77
- if (activeTab === 'whatsNew') {
78
- if (input === 'j' || key.downArrow) {
79
- setScrollOffset(prev => Math.min(prev + 1, maxScroll));
80
- return;
81
- }
82
- if (input === 'k' || key.upArrow) {
83
- setScrollOffset(prev => Math.max(prev - 1, 0));
84
- return;
85
- }
86
- }
87
171
  // Close modal
88
172
  if (key.escape || key.return || input === 'q' || input === ' ') {
89
173
  onClose();
90
174
  return;
91
175
  }
92
176
  });
93
- const formatTitle = (title) => theme.style.headerUppercase ? title.toUpperCase() : title;
94
- 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 === 'info', color: activeTab === 'info' ? theme.colors.secondary : theme.colors.textMuted, inverse: activeTab === 'info', children: [' ', formatTitle(help.infoTab), ' '] }), _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, {}))) : activeTab === 'info' ? (_jsx(InfoContent, {})) : (_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
95
- ? `j/k: scroll | ${help.tabHint} | ${help.closeHint}`
96
- : `${help.tabHint} | ${help.closeHint}` }) })] }));
97
- }
98
- function GTDKeybindingsContent() {
99
- const i18n = t();
100
- const help = i18n.tui.help;
101
- const theme = useTheme();
102
- const formatTitle = (title) => theme.style.headerUppercase ? title.toUpperCase() : title;
103
- 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(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "u" }), " ", help.undo] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "Ctrl+r" }), " ", help.redo] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.projects) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "p" }), " ", help.makeProject] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "P" }), " ", help.linkToProject] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "Enter" }), " ", help.openProject] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "Esc/b" }), " ", help.backFromProject] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.settings) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "T" }), " ", help.changeTheme] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "V" }), " ", help.changeViewMode] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "L" }), " ", help.changeLanguage] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.other) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "/" }), " ", help.searchTasks] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "?" }), " ", help.showHelp] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "q" }), " ", help.quit] })] })] })] }));
104
- }
105
- function KanbanKeybindingsContent() {
106
- const i18n = t();
107
- const help = i18n.tui.kanbanHelp;
108
- const theme = useTheme();
109
- const formatTitle = (title) => theme.style.headerUppercase ? title.toUpperCase() : title;
110
- 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(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "u" }), " Undo"] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "Ctrl+r" }), " Redo"] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.settings) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "T" }), " ", help.changeTheme] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "V" }), " ", help.changeViewMode] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "L" }), " ", help.changeLanguage] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(help.other) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "/" }), " ", help.searchTasks] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "?" }), " ", help.showHelp] }), _jsxs(Text, { color: theme.colors.text, children: [_jsx(Text, { color: theme.colors.textHighlight, children: "q" }), " ", help.quit] })] })] })] }));
111
- }
112
- function InfoContent() {
113
- const i18n = t();
114
- const info = i18n.tui.info;
115
- const theme = useTheme();
116
- const formatTitle = (title) => theme.style.headerUppercase ? title.toUpperCase() : title;
117
- const config = loadConfig();
118
- const tursoEnabled = isTursoEnabled();
119
- const tursoConfig = tursoEnabled ? getTursoConfig() : undefined;
120
- const dbPath = getDbPath();
121
- // Get Turso host for display
122
- const tursoUrl = tursoConfig ? (() => {
123
- try {
124
- return new URL(tursoConfig.url).host;
125
- }
126
- catch {
127
- return tursoConfig.url;
128
- }
129
- })() : '';
130
- return (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(info.settings) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsxs(Text, { color: theme.colors.textHighlight, children: [info.theme, ":"] }), " ", config.theme] }), _jsxs(Text, { color: theme.colors.text, children: [_jsxs(Text, { color: theme.colors.textHighlight, children: [info.language, ":"] }), " ", config.locale] }), _jsxs(Text, { color: theme.colors.text, children: [_jsxs(Text, { color: theme.colors.textHighlight, children: [info.viewMode, ":"] }), " ", config.viewMode] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(info.database) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsxs(Text, { color: theme.colors.textHighlight, children: [info.dbType, ":"] }), " ", tursoEnabled ? info.turso : info.local] }), _jsxs(Text, { color: theme.colors.text, children: [_jsxs(Text, { color: theme.colors.textHighlight, children: [info.dbPath, ":"] }), " ", dbPath] }), tursoEnabled && tursoUrl && (_jsxs(Text, { color: theme.colors.text, children: [_jsxs(Text, { color: theme.colors.textHighlight, children: [info.tursoUrl, ":"] }), " ", tursoUrl] }))] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(info.paths) }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.text, children: [_jsxs(Text, { color: theme.colors.textHighlight, children: [info.configFile, ":"] }), " ", CONFIG_FILE] }), _jsxs(Text, { color: theme.colors.text, children: [_jsxs(Text, { color: theme.colors.textHighlight, children: [info.dataDir, ":"] }), " ", DATA_DIR] })] })] })] }));
131
- }
132
- function WhatsNewContent({ items, scrollOffset, visibleLines, maxScroll }) {
133
- const i18n = t();
134
- const whatsNew = i18n.tui.whatsNew;
135
- const theme = useTheme();
136
- const formatTitle = (title) => theme.style.headerUppercase ? title.toUpperCase() : title;
137
- const getSectionLabel = (type) => {
138
- const labels = {
139
- Added: whatsNew.added,
140
- Changed: whatsNew.changed,
141
- Deprecated: whatsNew.deprecated,
142
- Removed: whatsNew.removed,
143
- Fixed: whatsNew.fixed,
144
- Security: whatsNew.security,
145
- };
146
- return labels[type] || type;
147
- };
148
- if (items.length === 0) {
149
- return (_jsx(Box, { justifyContent: "center", paddingY: 2, children: _jsx(Text, { color: theme.colors.textMuted, children: whatsNew.noChanges }) }));
150
- }
151
- const visibleItems = items.slice(scrollOffset, scrollOffset + visibleLines);
177
+ const visibleContent = currentContent.slice(scrollOffset, scrollOffset + VISIBLE_LINES);
152
178
  const showScrollUp = scrollOffset > 0;
153
179
  const showScrollDown = scrollOffset < maxScroll;
154
- return (_jsxs(Box, { flexDirection: "column", children: [showScrollUp && (_jsx(Text, { color: theme.colors.textMuted, children: " \u25B2 scroll up" })), visibleItems.map((item, index) => {
155
- if (item.type === 'version') {
156
- 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}`));
157
- }
158
- if (item.type === 'section') {
159
- return (_jsxs(Text, { color: theme.colors.secondary, children: [' ', formatTitle(getSectionLabel(item.content)), ":"] }, `${item.content}-${index}`));
160
- }
161
- return (_jsxs(Text, { color: theme.colors.text, children: [' ', "\u2022 ", item.content] }, `${item.content}-${index}`));
162
- }), showScrollDown && (_jsx(Text, { color: theme.colors.textMuted, children: " \u25BC scroll down" }))] }));
180
+ const renderLine = (line, index) => {
181
+ switch (line.type) {
182
+ case 'header':
183
+ return (_jsx(Text, { bold: true, color: theme.colors.accent, children: formatTitle(line.value) }, index));
184
+ case 'key':
185
+ return (_jsxs(Text, { color: theme.colors.text, children: [' ', _jsx(Text, { color: theme.colors.textHighlight, children: (line.key || '').padEnd(10) }), " ", line.value] }, index));
186
+ case 'version':
187
+ return (_jsxs(Text, { bold: true, color: theme.colors.accent, children: ["v", line.value, " ", line.date && _jsxs(Text, { color: theme.colors.textMuted, children: ["(", line.date, ")"] })] }, index));
188
+ case 'section':
189
+ return (_jsxs(Text, { color: theme.colors.secondary, children: [' ', formatTitle(line.value), ":"] }, index));
190
+ case 'item':
191
+ return (_jsxs(Text, { color: theme.colors.text, children: [' ', "\u2022 ", line.value] }, index));
192
+ default:
193
+ return (_jsx(Text, { color: theme.colors.text, children: line.value }, index));
194
+ }
195
+ };
196
+ 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 === 'info', color: activeTab === 'info' ? theme.colors.secondary : theme.colors.textMuted, inverse: activeTab === 'info', children: [' ', formatTitle(help.infoTab), ' '] }), _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), ' '] })] }) }), _jsxs(Box, { flexDirection: "column", height: VISIBLE_LINES + 2, children: [showScrollUp && (_jsx(Text, { color: theme.colors.textMuted, children: " \u25B2 scroll up (k)" })), !showScrollUp && _jsx(Text, { children: " " }), visibleContent.map((line, index) => renderLine(line, index)), showScrollDown && (_jsx(Text, { color: theme.colors.textMuted, children: " \u25BC scroll down (j)" })), !showScrollDown && _jsx(Text, { children: " " })] }), _jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsxs(Text, { color: theme.colors.textMuted, children: [maxScroll > 0 ? `j/k: scroll | ` : '', help.tabHint, " | ", help.closeHint] }) })] }));
163
197
  }
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useState, useEffect, useCallback } from 'react';
3
3
  import { Box, Text, useInput, useApp } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
- import { eq, and, inArray } from 'drizzle-orm';
5
+ import { eq, and, inArray, gte } from 'drizzle-orm';
6
6
  import { v4 as uuidv4 } from 'uuid';
7
7
  import { KanbanColumn } from './KanbanColumn.js';
8
8
  import { HelpModal } from './HelpModal.js';
@@ -72,11 +72,13 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
72
72
  .select()
73
73
  .from(schema.tasks)
74
74
  .where(and(inArray(schema.tasks.status, ['next', 'waiting']), eq(schema.tasks.isProject, false)));
75
- // Done: done (non-project tasks)
75
+ // Done: done (non-project tasks) - only show last week
76
+ const oneWeekAgo = new Date();
77
+ oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
76
78
  let doneTasks = await db
77
79
  .select()
78
80
  .from(schema.tasks)
79
- .where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false)));
81
+ .where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false), gte(schema.tasks.updatedAt, oneWeekAgo)));
80
82
  // Load projects for linking
81
83
  const projectTasks = await db
82
84
  .select()
@@ -189,7 +191,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
189
191
  setMode('normal');
190
192
  }
191
193
  }, [tasks]);
192
- const addTask = useCallback(async (title) => {
194
+ const addTask = useCallback(async (title, context) => {
193
195
  if (!title.trim())
194
196
  return;
195
197
  const now = new Date();
@@ -199,6 +201,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
199
201
  id: taskId,
200
202
  title: title.trim(),
201
203
  status: 'inbox', // New tasks go to inbox (which maps to TODO)
204
+ context: context || null,
202
205
  createdAt: now,
203
206
  updatedAt: now,
204
207
  },
@@ -249,7 +252,8 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
249
252
  return;
250
253
  }
251
254
  if (value.trim()) {
252
- await addTask(value);
255
+ // Pass contextFilter when adding a task, so it inherits the current filter context
256
+ await addTask(value, contextFilter && contextFilter !== '' ? contextFilter : null);
253
257
  }
254
258
  setInputValue('');
255
259
  setMode('normal');
@@ -711,7 +715,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
711
715
  : (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? (i18n.tui.context?.none || 'none') : contextFilter] }))] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), (mode === 'task-detail' || mode === 'add-comment' || mode === 'select-project') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📋 ' : '>> ', i18n.tui.taskDetailTitle || 'Task Details'] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ", ", i18n.tui.commentHint || 'i: add comment', ")"] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(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') })] }), selectedTask.dueDate && (_jsxs(Text, { color: theme.colors.textMuted, children: ["Due: ", selectedTask.dueDate.toLocaleDateString()] }))] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.comments || 'Comments', " (", taskComments.length, ")"] }) }), _jsx(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, minHeight: 5, children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
712
716
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
713
717
  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));
714
- })) }), mode === 'add-comment' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.addComment || 'New comment: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'select-project' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", selectedTask.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, project.title] }, project.id))) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] }))] })) : mode === 'search' ? (_jsx(_Fragment, { children: searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery })) })) : 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) => {
718
+ })) }), mode === 'add-comment' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.addComment || 'New comment: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'select-project' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", selectedTask.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, project.title] }, project.id))) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] }))] })) : mode === 'search' ? (_jsx(_Fragment, { children: searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery, viewMode: "kanban" })) })) : 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) => {
715
719
  const label = ctx === 'all'
716
720
  ? (i18n.tui.context?.all || 'All')
717
721
  : ctx === 'none'
@@ -1,8 +1,60 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { useTheme } from '../theme/index.js';
4
+ // Round border characters (DQ style)
5
+ const BORDER = {
6
+ topLeft: '╭',
7
+ topRight: '╮',
8
+ bottomLeft: '╰',
9
+ bottomRight: '╯',
10
+ horizontal: '─',
11
+ vertical: '│',
12
+ };
13
+ const SHADOW = '░';
14
+ // Calculate display width of string (full-width chars = 2, half-width = 1)
15
+ function getDisplayWidth(str) {
16
+ let width = 0;
17
+ for (const char of str) {
18
+ const code = char.charCodeAt(0);
19
+ if ((code >= 0x1100 && code <= 0x115F) ||
20
+ (code >= 0x2E80 && code <= 0x9FFF) ||
21
+ (code >= 0xAC00 && code <= 0xD7AF) ||
22
+ (code >= 0xF900 && code <= 0xFAFF) ||
23
+ (code >= 0xFE10 && code <= 0xFE1F) ||
24
+ (code >= 0xFE30 && code <= 0xFE6F) ||
25
+ (code >= 0xFF00 && code <= 0xFF60) ||
26
+ (code >= 0xFFE0 && code <= 0xFFE6) ||
27
+ (code >= 0x20000 && code <= 0x2FFFF)) {
28
+ width += 2;
29
+ }
30
+ else {
31
+ width += 1;
32
+ }
33
+ }
34
+ return width;
35
+ }
4
36
  export function KanbanColumn({ title, tasks, isActive, selectedTaskIndex, columnIndex, }) {
5
37
  const theme = useTheme();
38
+ const isLastColumn = columnIndex === 2;
39
+ const showShadow = theme.uiStyle === 'titled-box' && isLastColumn;
40
+ // Use TitledBox style for dragon-quest theme
41
+ if (theme.uiStyle === 'titled-box') {
42
+ const color = isActive ? theme.colors.borderActive : theme.colors.border;
43
+ const shadowColor = theme.colors.muted;
44
+ const titleText = `${title} (${tasks.length})`;
45
+ const titleLength = getDisplayWidth(titleText);
46
+ // Fixed width for each column (will be stretched by flexGrow)
47
+ const innerWidth = 24;
48
+ const leftDashes = 2;
49
+ const titlePadding = 2;
50
+ const rightDashes = Math.max(2, innerWidth - leftDashes - titlePadding - titleLength);
51
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, flexBasis: 0, marginRight: columnIndex < 2 ? 1 : 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: color, children: BORDER.topLeft }), _jsxs(Text, { color: color, children: [BORDER.horizontal.repeat(leftDashes), " "] }), _jsx(Text, { color: isActive ? theme.colors.accent : theme.colors.textMuted, bold: isActive, children: titleText }), _jsxs(Text, { color: color, children: [" ", BORDER.horizontal.repeat(rightDashes)] }), _jsx(Text, { color: color, children: BORDER.topRight }), showShadow && _jsx(Text, { children: " " })] }), _jsxs(Box, { flexDirection: "column", minHeight: 8, children: [tasks.length === 0 ? (_jsxs(Box, { children: [_jsx(Text, { color: color, children: BORDER.vertical }), _jsx(Box, { paddingX: 1, flexGrow: 1, children: _jsx(Text, { color: theme.colors.textMuted, italic: true, children: "-" }) }), _jsx(Text, { color: color, children: BORDER.vertical }), showShadow && _jsx(Text, { color: shadowColor, children: SHADOW })] })) : (tasks.map((task, index) => {
52
+ const isSelected = isActive && index === selectedTaskIndex;
53
+ const shortId = task.id.slice(0, 6);
54
+ return (_jsxs(Box, { children: [_jsx(Text, { color: color, children: BORDER.vertical }), _jsx(Box, { paddingX: 1, flexGrow: 1, children: _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix, "[", shortId, "] ", task.title] }) }), _jsx(Text, { color: color, children: BORDER.vertical }), showShadow && _jsx(Text, { color: shadowColor, children: SHADOW })] }, task.id));
55
+ })), Array.from({ length: Math.max(0, 8 - Math.max(1, tasks.length)) }).map((_, i) => (_jsxs(Box, { children: [_jsx(Text, { color: color, children: BORDER.vertical }), _jsx(Box, { paddingX: 1, flexGrow: 1, children: _jsx(Text, { children: " " }) }), _jsx(Text, { color: color, children: BORDER.vertical }), showShadow && _jsx(Text, { color: shadowColor, children: SHADOW })] }, `empty-${i}`)))] }), _jsxs(Box, { children: [_jsx(Text, { color: color, children: BORDER.bottomLeft }), _jsx(Text, { color: color, children: BORDER.horizontal.repeat(innerWidth) }), _jsx(Text, { color: color, children: BORDER.bottomRight }), showShadow && _jsx(Text, { color: shadowColor, children: SHADOW })] }), showShadow && (_jsx(Box, { children: _jsxs(Text, { color: shadowColor, children: [" ", SHADOW.repeat(innerWidth + 2)] }) }))] }));
56
+ }
57
+ // Default style
6
58
  return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, flexBasis: 0, borderStyle: theme.borders.list, borderColor: isActive ? theme.colors.borderActive : theme.colors.border, marginRight: columnIndex < 2 ? 1 : 0, children: [_jsx(Box, { paddingX: 1, justifyContent: "center", borderStyle: undefined, children: _jsxs(Text, { bold: true, color: isActive ? theme.colors.primary : theme.colors.textMuted, inverse: isActive && theme.style.tabActiveInverse, children: [title, " (", tasks.length, ")"] }) }), _jsx(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, minHeight: 8, children: tasks.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: "-" })) : (tasks.map((task, index) => {
7
59
  const isSelected = isActive && index === selectedTaskIndex;
8
60
  const shortId = task.id.slice(0, 6);
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ type SettingsMode = 'none' | 'theme-select' | 'mode-select' | 'lang-select';
3
+ interface KanbanDQProps {
4
+ onOpenSettings?: (mode: SettingsMode) => void;
5
+ }
6
+ export declare function KanbanDQ({ onOpenSettings }: KanbanDQProps): React.ReactElement;
7
+ export {};